Curriculum1 of 10
+ Module 09

React Fundamentals

Build a small React app and reason about state

Phase 1 · JavaScript and React · 10 sections · about 8 minutes

+ 01 / 10

What's a component?

A component is a JavaScript function that returns one JSX root element. That root wraps everything else.

JSX is plain JavaScript pretending to be HTML. When you write <Button>Click me</Button>, React turns it into a function call: Button({ children: 'Click me' }).

+ JSX
1function Greeting() {
2 return <h1>Hello, world</h1>;
3}
4 
5function App() {
6 return <Greeting />;
7}

The rule is strict: one root. If you need two siblings, you wrap them. A Fragment (<>...</>) is an invisible wrapper that counts.

+ JSX
1function Card() {
2 return (
3 <>
4 <h2>Title</h2>
5 <p>Description</p>
6 </>
7 );
8}

React calls your function when it needs to see what should appear on screen. Think of it as "what does this component look like right now?"

+ 02 / 10

useState and the re-render loop

useState is how you tell React that a component has state: data that can change. When it changes, React re-runs your function to see the new output.

+ JSX
1import { useState } from "react";
2 
3function Counter() {
4 const [count, setCount] = useState(0);
5 
6 return (
7 <div>
8 <p>Count: {count}</p>
9 <button onClick={() => setCount(count + 1)}>Increment</button>
10 </div>
11 );
12}

Here's what happens: user clicks the button. onClick fires. setCount(count + 1) runs. React schedules a re-render. React calls Counter() again. This time useState(0) returns the new value, 1. The JSX says "Count: 1". The screen updates.

The loop is: event → setState → re-render → new output.

That description is accurate and it is also the part everyone has to see before they believe it. Two things in it sound contradictory: the whole function runs again from the top on every keystroke, and yet the value in state survives. Run it yourself.

+ Try itrender 1

The component function runs again. The state does not.

function UserSearch() {
const [query, setQuery] = useState("");
const matches = users.filter(
(user) => user.includes(query)
);
return (
<input value={query} onChange={...} />
{matches.map((u) => <li>{u}</li>)}
);
}

Type below to run the function.

idle
State, kept""
Renders1
Rebuilt every render
  • Ada Lovelace
  • Grace Hopper
  • Alan Turing
  • Katherine Johnson

Type a letter and watch the whole function re-run top to bottom. The one thing that survives is the value useState hands back.

Watch line 2 in particular. On the first render useState("") hands back the empty string you passed in. On every render after that it hands back whatever the state currently is, and ignores the argument entirely. That is the only line in the function with a memory. Everything else, including the filtered list, is built again from nothing each time.

+ Checkpoint

On the fourth render, what does useState("") return?

You can call useState multiple times in one component. Each call gets its own pair.

+ JSX
1function Form() {
2 const [name, setName] = useState("");
3 const [email, setEmail] = useState("");
4 
5 return (
6 <div>
7 <input value={name} onChange={(e) => setName(e.target.value)} />
8 <input value={email} onChange={(e) => setEmail(e.target.value)} />
9 </div>
10 );
11}

Never set state directly. Always use the setter function. React needs to know state changed so it can re-render.

+ 03 / 10

Derived vs. stored state

Derived state is computed from other state. Stored state lives directly in useState.

If you have both weight and height, don't store BMI in state. Calculate it every render.

+ JSX
1function BMI() {
2 const [weight, setWeight] = useState(70);
3 const [height, setHeight] = useState(1.75);
4 
5 // Derived: calculate it fresh every render
6 const bmi = weight / (height * height);
7 
8 return (
9 <div>
10 <input
11 value={weight}
12 onChange={(e) => setWeight(Number(e.target.value))}
13 />
14 <input
15 value={height}
16 onChange={(e) => setHeight(Number(e.target.value))}
17 />
18 <p>BMI: {bmi.toFixed(1)}</p>
19 </div>
20 );
21}

Why? Because now weight and height are always in sync with BMI. If you stored BMI separately, you'd have to update it manually whenever weight or height changed. Easy to forget. State goes stale.

The rule: if it can be computed from other state, don't store it.

+ Checkpoint

A search page holds the query in state. Should the filtered results also live in useState?

+ 04 / 10

The controlled input

An input is controlled when React holds its value in state. You set the value prop and handle onChange to update state.

+ JSX
1function SearchBox() {
2 const [query, setQuery] = useState("");
3 
4 return (
5 <input
6 type="text"
7 value={query}
8 onChange={(e) => setQuery(e.target.value)}
9 placeholder="Search..."
10 />
11 );
12}

React is now the source of truth for the input's value. The input can't drift out of sync with your state.

An uncontrolled input leaves the value to the browser. You read it when you need it with ref. Avoid refs for inputs unless you have a specific reason, like focus management or file uploads.

Controlled inputs are predictable. They're the default in React.

+ 05 / 10

Props: data parent to child

Props are how a parent component passes data to a child. They're function arguments.

+ JSX
1function Header({ title, subtitle }) {
2 return (
3 <header>
4 <h1>{title}</h1>
5 <p>{subtitle}</p>
6 </header>
7 );
8}
9 
10function App() {
11 return <Header title="Welcome" subtitle="React basics" />;
12}

Props are read-only from the child's perspective. You can't call setTitle(...) inside Header and expect the parent to update. That violates the one-way flow: parent owns the data, parent can change it, child reads it.

If a child needs to change data, it calls a function the parent passed down.

+ JSX
1function Button({ label, onClick }) {
2 return <button onClick={onClick}>{label}</button>;
3}
4 
5function App() {
6 const [count, setCount] = useState(0);
7 return <Button label="Click me" onClick={() => setCount(count + 1)} />;
8}

Here's a small naming convention baked into the language: onClick is reserved for the click handler prop. If you try onClick on a div, it won't work. That's "chosen, not fixed." The platform chose camelCase and reserved these names. Your code has to match.

+ 06 / 10

Composition into a tree

Real apps are hierarchies of components. Each piece handles its own slice of the UI.

+ JSX
1function SearchResults({ results }) {
2 if (results.length === 0) return <p>No results</p>;
3 
4 return (
5 <ul>
6 {results.map((item) => (
7 <ResultItem key={item.id} item={item} />
8 ))}
9 </ul>
10 );
11}
12 
13function ResultItem({ item }) {
14 return <li>{item.name}</li>;
15}
16 
17function SearchPage() {
18 const [query, setQuery] = useState("");
19 const results = fakeSearch(query);
20 
21 return (
22 <div>
23 <input value={query} onChange={(e) => setQuery(e.target.value)} />
24 <SearchResults results={results} />
25 </div>
26 );
27}

Each component has one job. SearchPage manages the query. SearchResults renders a list. ResultItem renders one item. The tree is clear.

When the query changes, SearchPage re-renders, passes new results to SearchResults, which re-renders its children. This flows naturally.

Always add a key prop when rendering lists. It helps React track which item is which if the list reorders.

+ 07 / 10

Lifting state up

If two sibling components need to share state, move that state to their common parent.

+ JSX
1function Tab({ label, isActive, onClick }) {
2 return (
3 <button
4 onClick={onClick}
5 style={{ fontWeight: isActive ? "bold" : "normal" }}
6 >
7 {label}
8 </button>
9 );
10}
11 
12function Tabs() {
13 const [activeTab, setActiveTab] = useState("home");
14 
15 return (
16 <div>
17 <div>
18 <Tab
19 label="Home"
20 isActive={activeTab === "home"}
21 onClick={() => setActiveTab("home")}
22 />
23 <Tab
24 label="About"
25 isActive={activeTab === "about"}
26 onClick={() => setActiveTab("about")}
27 />
28 </div>
29 <div>
30 {activeTab === "home" && <p>Home content</p>}
31 {activeTab === "about" && <p>About content</p>}
32 </div>
33 </div>
34 );
35}

Both tabs need to know which one is active. Instead of storing it in each tab separately, store it in Tabs and pass it down as props. Now they always agree.

This pattern scales to any number of siblings. The parent is the single source of truth.

+ Checkpoint

Two sibling components need to show the same selected item. Where does that selection belong?

+ 08 / 10

The four states

Every async operation has four states: idle (waiting to start), loading (in flight), empty (done, no data), error (done, failure).

Real UIs handle all four.

+ JSX
1function BookSearch() {
2 const [query, setQuery] = useState("");
3 const [state, setState] = useState("idle");
4 const [results, setResults] = useState([]);
5 const [error, setError] = useState(null);
6 
7 const handleSearch = async (q) => {
8 if (!q) {
9 setState("idle");
10 setResults([]);
11 return;
12 }
13 
14 setState("loading");
15 try {
16 const data = await fetch(`/api/books?q=${q}`).then((r) => r.json());
17 if (data.length === 0) {
18 setState("empty");
19 setResults([]);
20 } else {
21 setState("idle");
22 setResults(data);
23 }
24 } catch (err) {
25 setState("error");
26 setError(err.message);
27 }
28 };
29 
30 return (
31 <div>
32 <input
33 value={query}
34 onChange={(e) => {
35 setQuery(e.target.value);
36 handleSearch(e.target.value);
37 }}
38 placeholder="Search books..."
39 />
40 
41 {state === "idle" && results.length > 0 && (
42 <ul>
43 {results.map((book) => (
44 <li key={book.id}>{book.title}</li>
45 ))}
46 </ul>
47 )}
48 
49 {state === "loading" && <p>Searching...</p>}
50 {state === "empty" && <p>No books found</p>}
51 {state === "error" && <p>Error: {error}</p>}
52 </div>
53 );
54}

This is tight. Every state has its own UI. No results render until data arrives. Loading UI shows immediately. Errors have a home.

The four states are a mental model. Write it down: idle, loading, empty, error. Then check your UI handles all four. Most bugs live in the states you skipped.

+ Checkpoint

A search returns zero results and your UI renders nothing at all. Which state did you skip?

+ 09 / 10

Build: Search interaction in React

Let's build a real search experience split into components. A search box, a results list, and proper state handling.

+ JSX
1function SearchBox({ query, onQueryChange }) {
2 return (
3 <input
4 type="text"
5 value={query}
6 onChange={(e) => onQueryChange(e.target.value)}
7 placeholder="Search books..."
8 />
9 );
10}
11 
12function SearchResults({ state, results, error }) {
13 if (state === "loading") return <p>Searching...</p>;
14 if (state === "empty") return <p>No books found</p>;
15 if (state === "error") return <p>Error: {error}</p>;
16 if (results.length === 0) return null;
17 
18 return (
19 <ul>
20 {results.map((book) => (
21 <li key={book.id}>
22 {book.title} by {book.author}
23 </li>
24 ))}
25 </ul>
26 );
27}
28 
29function BookSearch() {
30 const [query, setQuery] = useState("");
31 const [state, setState] = useState("idle");
32 const [results, setResults] = useState([]);
33 const [error, setError] = useState(null);
34 
35 const handleSearch = async (q) => {
36 setQuery(q);
37 
38 if (!q.trim()) {
39 setState("idle");
40 setResults([]);
41 return;
42 }
43 
44 setState("loading");
45 try {
46 const response = await fetch(`/api/books?q=${encodeURIComponent(q)}`);
47 const data = await response.json();
48 
49 if (data.length === 0) {
50 setState("empty");
51 } else {
52 setState("idle");
53 setResults(data);
54 }
55 } catch (err) {
56 setState("error");
57 setError(err.message);
58 }
59 };
60 
61 return (
62 <div>
63 <SearchBox query={query} onQueryChange={handleSearch} />
64 <SearchResults state={state} results={results} error={error} />
65 </div>
66 );
67}

The structure: BookSearch owns all state. It passes the query to SearchBox and the results to SearchResults. When you type, SearchBox calls onQueryChange, which runs the async fetch and updates state. SearchResults re-renders with the new state.

Each component is small and testable. The flow is clear: parent to child via props, child to parent via callbacks.

+ 10 / 10

Checkpoint

Before you move on, make sure you can answer these aloud.

No derived data in useState. Look at your state shape. If something can be calculated from other state, remove it. Keep only the irreducible facts.

Justify the state shape aloud. Say why each piece of state is there. If you can't explain it in one sentence, it might be derived or unnecessary.

Handle all four states. Write code for idle, loading, empty, and error. Real apps need all four. If your UI doesn't show loading, users will think it froze.

Hand-write a prop. Create a component with three props. Pass them from a parent. Don't skip steps. The muscle memory matters.

Also: notice how onClick is spelled camelCase. That's not a bug. That's the platform. React chose it. Your code has to match. There's no "correct" spelling in the abstract. There's only what the platform chose, and your code following it. This is "chosen, not fixed," and it's how you learn to work with systems instead of against them.

+ Up nextTesting React UIPreviouslyTypeScript for UI