AdvancedJavaScript · Lesson 9 of 9

Introduction to React

Components, props, JSX, state with useState, lists with keys, controlled forms and loading data with useEffect.

React builds interfaces from components: functions that take props and return JSX, an HTML-like syntax that compiles to JavaScript. Instead of changing the DOM yourself, you describe what the page should look like for the current data, and React updates the DOM when the data changes. Vite creates a ready-to-run project with a fast dev server.

useState stores data that changes, such as the list of students or the text in an input. Calling the setter re-renders the component. Never mutate state (students.push); create a new array or object instead. Inputs whose value comes from state and whose onChange updates it are called controlled inputs. Values you can compute from state, such as a filtered list, should be computed during render rather than stored.

Lists need a stable key on each item so React can tell rows apart. useEffect runs code after rendering, which suits loading data; return a cleanup function (here, aborting the fetch) for when the component goes away. Development mode with <StrictMode> runs effects twice on purpose to reveal missing cleanups. npm run build produces static files you can deploy anywhere.

TerminalShell
npm create vite@latest results-app -- --template react
cd results-app
npm install
npm run dev          # opens http://localhost:5173 and reloads as you save
public/students.jsonJSON
[
  { "id": 1, "name": "Amina Hassan", "score": 88 },
  { "id": 2, "name": "Juma Said", "score": 42 },
  { "id": 3, "name": "Neema Kimaro", "score": 71 }
]
src/main.jsxJSX
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App.jsx";

createRoot(document.getElementById("root")).render(
  <StrictMode>
    <App />
  </StrictMode>,
);
src/StudentList.jsxJSX
// A component is a function that takes props and returns JSX.
function StudentRow({ student }) {
  const passed = student.score >= 30;
  return (
    <li>
      {student.name} — {student.score} {passed ? "✔" : <strong>(needs support)</strong>}
    </li>
  );
}

export default function StudentList({ students }) {
  if (students.length === 0) return <p>No students match.</p>;
  return (
    <ul>
      {students.map((s) => (
        <StudentRow key={s.id} student={s} />   // key lets React track each row
      ))}
    </ul>
  );
}
src/App.jsxJSX
import { useEffect, useState } from "react";
import StudentList from "./StudentList.jsx";

export default function App() {
  const [students, setStudents] = useState([]);
  const [status, setStatus] = useState("loading");     // "loading" | "ready" | "error"
  const [query, setQuery] = useState("");
  const [name, setName] = useState("");
  const [score, setScore] = useState("");

  // Runs after the first render; the cleanup cancels the request if the component goes away.
  useEffect(() => {
    const controller = new AbortController();
    fetch("/students.json", { signal: controller.signal })
      .then((res) => {
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        return res.json();
      })
      .then((data) => {
        setStudents(data);
        setStatus("ready");
      })
      .catch((err) => {
        if (err.name !== "AbortError") setStatus("error");
      });
    return () => controller.abort();
  }, []);

  function handleSubmit(event) {
    event.preventDefault();
    if (name.trim().length < 2 || score === "") return;
    // Never mutate state: build a new array so React sees the change.
    setStudents([...students, { id: Date.now(), name: name.trim(), score: Number(score) }]);
    setName("");
    setScore("");
  }

  // Derived values are computed during render, not stored in state.
  const visible = students.filter((s) => s.name.toLowerCase().includes(query.toLowerCase()));

  return (
    <main>
      <h1>Form 4 results</h1>

      <form onSubmit={handleSubmit}>
        <input aria-label="Name" placeholder="Name" value={name} onChange={(e) => setName(e.target.value)} />
        <input aria-label="Score" type="number" min="0" max="100" value={score} onChange={(e) => setScore(e.target.value)} />
        <button>Add</button>
      </form>

      <input aria-label="Search" placeholder="Search..." value={query} onChange={(e) => setQuery(e.target.value)} />

      {status === "loading" && <p>Loading…</p>}
      {status === "error" && <p role="alert">Could not load students.</p>}
      {status === "ready" && <StudentList students={visible} />}
    </main>
  );
}

Key points

  • Components are functions of props that return JSX; React updates the DOM for you.
  • Keep changing data in useState, always replace rather than mutate it, and derive the rest during render.
  • Give list items a stable key, and load data in useEffect with a cleanup function.

Exercise

Extend the app: show the number of students and the class average, add a Remove button on every row, and save the list to localStorage so it survives a reload (load the JSON file only when nothing is saved yet).

Show solution

Try the exercise yourself first — then compare your approach with this one.

useState(() => loadSaved() ?? []) reads localStorage only on the first render. The fetch effect runs only while the status is "loading", that is when nothing was saved. A second effect saves the list whenever it changes. The average is derived from students during render, and the updater form setStudents(prev => ...) always works on the latest list.

src/App.jsxJSX
import { useEffect, useState } from "react";

const STORAGE_KEY = "results-app.students";

// Read saved students once, when the component first renders.
function loadSaved() {
  try {
    return JSON.parse(localStorage.getItem(STORAGE_KEY)) ?? null;
  } catch {
    return null;
  }
}

export default function App() {
  const [students, setStudents] = useState(() => loadSaved() ?? []);
  const [status, setStatus] = useState(() => (loadSaved() ? "ready" : "loading"));
  const [name, setName] = useState("");
  const [score, setScore] = useState("");

  useEffect(() => {
    if (status !== "loading") return;            // already loaded from localStorage
    const controller = new AbortController();
    fetch("/students.json", { signal: controller.signal })
      .then((res) => (res.ok ? res.json() : Promise.reject(new Error(`HTTP ${res.status}`))))
      .then((data) => {
        setStudents(data);
        setStatus("ready");
      })
      .catch((err) => err.name !== "AbortError" && setStatus("error"));
    return () => controller.abort();
  }, [status]);

  // Save whenever the list changes (after the initial load).
  useEffect(() => {
    if (status === "ready") localStorage.setItem(STORAGE_KEY, JSON.stringify(students));
  }, [students, status]);

  function add(event) {
    event.preventDefault();
    if (name.trim().length < 2 || score === "") return;
    setStudents((prev) => [...prev, { id: Date.now(), name: name.trim(), score: Number(score) }]);
    setName("");
    setScore("");
  }

  function remove(id) {
    setStudents((prev) => prev.filter((s) => s.id !== id));
  }

  const average = students.length
    ? (students.reduce((sum, s) => sum + s.score, 0) / students.length).toFixed(1)
    : "–";

  if (status === "loading") return <p>Loading…</p>;
  if (status === "error") return <p role="alert">Could not load students.</p>;

  return (
    <main>
      <h1>Form 4 results</h1>
      <p>
        {students.length} students · class average <strong>{average}</strong>
      </p>
      <form onSubmit={add}>
        <input aria-label="Name" placeholder="Name" value={name} onChange={(e) => setName(e.target.value)} />
        <input aria-label="Score" type="number" min="0" max="100" value={score} onChange={(e) => setScore(e.target.value)} />
        <button>Add</button>
      </form>
      <ul>
        {students.map((s) => (
          <li key={s.id}>
            {s.name} — {s.score}{" "}
            <button type="button" onClick={() => remove(s.id)} aria-label={`Remove ${s.name}`}>
              Remove
            </button>
          </li>
        ))}
      </ul>
    </main>
  );
}

Check your understanding

  1. Why does students.push(newStudent); setStudents(students) fail to update the page?

  2. What is the key prop on list items for?

  3. When does useEffect(fn, []) run fn?

  4. Where should a filtered list (search results) live?

Ask AI