State: Components That Remember
18 minIntermediateReact
In this lesson
- ▸Explain why a normal variable can't update the screen
- ▸Create state with
useStateand update it with its setter - ▸Update arrays and objects by creating new ones instead of mutating
- ▸Use a functional update when the next value depends on the previous one
The big idea
State is data a component remembers between renders. When you call the setter, React runs your component again with the new value and updates the screen.
Props come from the parent. But some data belongs to the component itself and changes while the user interacts: a counter, a list of orders, whether a menu is open. That's state.
Why a normal variable doesn't work
A component is a function. Every time React renders it, the function runs from the top — so let cups = 0 starts at 0 every render. And changing a local variable doesn't tell React anything, so it never re-renders. The variable changes; the screen doesn't.
Forgets, and React isn't told
function Counter() {
let cups = 0;
function add() {
cups = cups + 1; // changes, but no re-render
}
return <button onClick={add}>{cups}</button>;
}Remembered, and triggers a re-render
import { useState } from "react";
function Counter() {
const [cups, setCups] = useState(0);
function add() {
setCups(cups + 1); // new value + re-render
}
return <button onClick={add}>{cups}</button>;
}- useState(initial)
- A hook — a special React function whose name starts with
use. It returns a pair: the current value and a function to change it. - const [cups, setCups]
- Array destructuring. Name them whatever you like; the convention is
thingandsetThing. - Re-render
- React calls your component function again. This time
useStatereturns the new value, and React updates only the parts of the DOM that changed.
Rules of hooks
Call useState at the top level of your component — not inside if, loops or nested functions. React tells hooks apart by the order they're called in, so the order must be the same on every render.
Rule 1: never mutate state
React decides whether to re-render by checking if the new value is a different value (it uses Object.is). If you push into the same array and pass that same array back, React sees "nothing changed" and skips the update. Always create a new array or object.
Mutates (no re-render)
orders.push("Order #3");
setOrders(orders); // same array!
user.name = "Siti";
setUser(user); // same object!New value (re-renders)
setOrders([...orders, "Order #3"]);
setUser({ ...user, name: "Siti" });
setOrders(orders.filter((o) => o !== "Order #1"));Rule 2: depend on the previous value? Pass a function
Inside one render, cups is a snapshot — a fixed number. Calling the setter doesn't change it; it asks React for a new render. So if you queue several updates from the same snapshot, they all compute from the same old number. Pass an updater function instead and React feeds it the latest value.
Adds 1, not 3 (cups is 0 all along)
function addThree() {
setCups(cups + 1); // 0 + 1
setCups(cups + 1); // 0 + 1
setCups(cups + 1); // 0 + 1
}Adds 3
function addThree() {
setCups((c) => c + 1); // 0 → 1
setCups((c) => c + 1); // 1 → 2
setCups((c) => c + 1); // 2 → 3
}The same snapshot problem shows up with timers: a setTimeout callback remembers the cups from the render where it was created. You'll fix exactly that in exercise 2.
Click +1 cup and open the Console: the variable goes up, but the big number stays at 0.
Replace let cups = 0 with state: const [cups, setCups] = useState(0). Then update addCup to call setCups(cups + 1). Do the same inside addCupLater for now (setCups(cups + 1) inside the timeout) — you'll look at it again in the next exercise.
Checks
- The counter starts at 0
- Clicking "+1 cup" updates the screen
Check understanding
What you learned
- ✓
const [value, setValue] = useState(initial)gives a component memory between renders. - ✓Calling the setter re-runs the component with the new value; React then updates the DOM.
- ✓Never mutate state — make new arrays/objects with spread:
[...list, x],{ ...obj, key: v }. - ✓When the next value depends on the previous one, use
setValue((prev) => …).
Console output from your code appears here.