State with useState
Adding memory to components, re-renders, state as a snapshot, updater functions and immutable updates.
Props come from outside a component. State is data a component owns and remembers between renders — a counter, the text in an input, whether a menu is open, the items in a cart. When state changes, React re-renders the component to show the new value. useState is the hook that makes this happen, and understanding how it works is the key to React.
Why regular variables don't work#
Two problems: changing a local variable doesn't tell React to re-render, and even if it did re-render, let count = 0 would run again and reset it. We need a value that persists between renders and triggers a re-render when it changes.
useState#
useState(0)declares a state variable with initial value0.- It returns a pair: the current value and a setter function. Name them
[something, setSomething]. - Calling
setCount(newValue)stores the new value and schedules a re-render. On the next render,useStatereturns the updated value.
A component can have as many state variables as it needs:
Rules of Hooks
Functions starting with use are hooks. Two rules:
- Call hooks only at the top level of a component (or custom hook) — never inside
if, loops or nested functions. - Call hooks only from React components or custom hooks, not regular functions.
React identifies each state variable by its call order, so the order must be the same on every render. The linter's hooks rule catches violations.
How a re-render works#
When you click the button:
setCount(1)tells React "next time,countis 1" and queues a render.- React calls
Counter()again. This timeuseState(0)returns[1, setCount](the initial value is ignored after the first render). - The new JSX says "Clicked 1 times", so React updates that text in the DOM.
Each render is like a snapshot: the props, state and handlers belong to that render.
State is a snapshot#
Setting state doesn't change the variable in the code that's already running — it requests a new render with a new value. React also batches all updates from one event into a single re-render.
Updater functions
When the next value depends on the previous one, pass a function to the setter. React queues it and gives it the latest pending value:
setCount(count + 1) means "set it to 1"; setCount(c => c + 1) means "add one to whatever it is". Use the updater form whenever you compute from the previous state — it's also safer with timers and async code.
Lazy initial state#
If the initial value is expensive to compute, pass a function — React only calls it on the first render:
Objects in state: replace, don't mutate#
React decides whether to re-render by comparing the old and new value with Object.is — for objects and arrays, that's a reference check. Mutating an object keeps the same reference, so React may skip the update. Always create a new object:
Spread copies the existing fields, then the computed key [e.target.name] overrides one of them. For nested objects, copy each level you change:
Arrays in state: use non-mutating methods#
A complete todo list:
Notice remaining isn't state — it's derived from todos during render. Don't store values you can calculate.
State is local and private#
Each instance of a component has its own state:
Clicking one counter doesn't affect the other. When two components need to share state, move it up to their common parent and pass it down as props — "lifting state up", covered in a later lesson.
React keeps state tied to the component's position in the tree. If a component is removed and later added back, its state starts fresh.
Choosing state well#
- Minimal: store the least you can; derive the rest (
remaining,fullName,filteredItems). - No duplicates: don't copy props into state unless you deliberately want to ignore later prop changes.
- Group related values that always change together (
{ x, y }), but keep unrelated ones separate. - Avoid contradictions: instead of
isLoadingandisErrorbooleans, consider onestatusvalue:"idle" | "loading" | "error" | "success".
Common mistakes#
- Expecting the state variable to change right after calling the setter.
- Mutating objects or arrays and calling the setter with the same reference.
- Calling hooks conditionally.
- Storing derived values in state and having them fall out of sync.
- Calling a setter during render (
setCount(count + 1)in the component body) — infinite loop.
What's next#
With state, your UI can change. Next, learn the patterns for showing different UI in different situations: conditional rendering.
Check your understanding
Quick quiz
1.What does
useState(0)return?2.
countis 0. A click handler runssetCount(count + 1); setCount(count + 1); setCount(count + 1);. What iscountafter the re-render?3.Why does
todos.push(newTodo); setTodos(todos);fail to update the screen?
Finished reading?
Mark this lesson complete to track your progress.