Section 3 · State & Events
useState Fundamentals
20 min3 min readCode playground
Lesson at a glance
Give components memory with useState, understand state as a per-render snapshot, use updater functions for successive updates, and replace arrays and objects instead of mutating them.
- 1useState
- 2Every render is a snapshot
- 3Arrays and objects: replace, don’t mutate
- 4Choosing the initial value
No video here — just the real thing: a written lesson, runnable examples, a quiz and practice.
Click a “Mark as watched” button in a plain function component and nothing happens. Here’s the code most people write first:
export default function MovieItem() {
let watched = false;
function handleClick() {
watched = !watched;
}
return (
<button onClick={handleClick}>
{watched ? 'Watched' : 'Mark as watched'}
</button>
);
}Two things go wrong. Changing a local variable doesn’t tell React anything happened, so it never renders again. And even if something else caused a render, let watched = false would run again and reset the value, because a component is a function and its local variables start fresh on every call. A component needs two things: memory that survives between renders, and a way to ask React to render again. That’s state.
useState
import { useState } from 'react';
export default function MovieItem({ title }) {
const [watched, setWatched] = useState(false);
return (
<li>
{title}
<button aria-pressed={watched} onClick={() => setWatched(!watched)}>
{watched ? 'Watched' : 'Mark as watched'}
</button>
</li>
);
}useState(false) declares one piece of state with an initial value of false. It returns a pair, which you destructure and name: the current value, and a setter function. The [thing, setThing] naming is a convention everyone follows; stick to it.
When you call setWatched(true), React stores the new value and schedules a re-render. It calls MovieItem again, and this time useState returns true. Your JSX is calculated from that, and React updates the DOM to match.
State belongs to a component instance, not to the function. Render <MovieItem /> three times and you get three independent watched values. Clicking one doesn’t affect the others.
Every render is a snapshot
This is the idea that makes state click. When React calls your component, it hands you the state for that render. Everything in that render, the JSX, the event handlers, all of it, sees those values. Setting state doesn’t change the variable you already have; it requests a new render with a new value.
That’s why this surprises people:
const [count, setCount] = useState(0);
function addThree() {
setCount(count + 1); // count is 0 → asks for 1
setCount(count + 1); // count is still 0 → asks for 1
setCount(count + 1); // still 0 → 1
}All three calls read the same snapshot, so the result is 1, not 3. When the next value depends on the previous one, pass an updater function instead of a value:
function addThree() {
setCount((c) => c + 1); // 0 → 1
setCount((c) => c + 1); // 1 → 2
setCount((c) => c + 1); // 2 → 3
}React queues the updaters and runs them in order, each receiving the result of the one before. You don’t need updaters everywhere; setWatched(!watched) in a click handler is fine. Use them when you set the same state more than once in a handler, or in code that runs later, such as a timer.
Arrays and objects: replace, don’t mutate
The Watchlist keeps an array of movie objects in state. React decides whether state changed by comparing the old and new value with Object.is. If you change an array in place and pass the same array back, React sees the same reference and may skip the render entirely.
So treat state as read-only and build new values:
const [movies, setMovies] = useState(INITIAL_MOVIES);
// Add: a new array with everything plus one more
setMovies([...movies, { id: crypto.randomUUID(), title: 'Dune', watched: false }]);
// Remove: a new array without one item
setMovies(movies.filter((m) => m.id !== id));
// Update one: a new array, with a new object for the changed item
setMovies(movies.map((m) => (m.id === id ? { ...m, watched: !m.watched } : m)));These three lines (spread to add, filter to remove, map with a spread to update) cover most of the state updates you’ll ever write for lists. Objects work the same way: setFilters({ ...filters, year: 2024 }).
Choosing the initial value
The argument to useState is only used on the first render. Later renders ignore it. If computing the initial value is expensive, such as reading and parsing saved data, pass a function instead, useState(() => loadMovies()), and React calls it only on the first render instead of on every render. You’ll use exactly that to load the Watchlist from localStorage in Section 4.
Now your components can remember things. Next, you’ll look closely at the events that change state: click, keyboard and the event object itself.
Key takeaways
useStatereturns the current value and a setter; calling the setter schedules a re-render with the new value.- Each render sees a fixed snapshot of state, so reading state right after setting it still gives the old value.
- Use an updater function,
setCount((c) => c + 1), when the next value depends on the previous one. - Never mutate state; create new arrays and objects with spread,
mapandfilterso React notices the change. - Call Hooks at the top level of a component, never inside conditions, loops or nested functions.