useState is small. The traps around it are not. This lesson is the trap map.
The shape
const [value, setValue] = useState(initialValue). The hook returns a tuple of [current value, setter function]. The current value is fresh on every render. The setter is stable — its identity doesn't change across renders, so you can pass it to dependency arrays without it triggering re-runs.
The lazy initializer
If initialValue is expensive to compute, use a function form: useState(() => computeIt()). React calls the function once on the initial render and ignores it forever after. The bare-call form useState(computeIt()) runs the computation every render even though React only uses the result from the first call.
Functional updates
If your new value depends on the previous value, pass a function to the setter: setCount(n => n + 1). This protects you from stale-closure bugs (where an event handler captures an old value and another update lands between captures). Always use the functional form when the new value depends on the old one.
State batching
React 19 batches all state updates that happen within the same event handler, effect, or async function call. setA(1); setB(2); in a click handler triggers one re-render, not two. You don't usually have to think about this — but if you're surprised that two updates produced one render, batching is why.
Objects and arrays — immutable updates only
State updates by reference equality. setUser(user) with the same object reference is a no-op. To update an object field, spread to a new object: setUser({ ...user, name: 'new' }). Same for arrays: setItems([...items, newItem]). Mutating the existing object/array and calling the setter won't trigger a re-render.
Multiple useState vs one big object
If two state values change together, group them: useState({ x: 0, y: 0 }). If they change independently, separate them: useState(0); useState(0). The rule of thumb: state that always updates together belongs together; state that updates independently splits.
fullName can be computed from firstName and lastName, don't store it in useState. Compute it in render: const fullName = `${firstName} ${lastName}`. Storing derived values is how state-sync bugs start.