useState Explained
How React's most-used hook stores state, triggers re-renders, and the update patterns that avoid its common pitfalls.
2 min read
useState is how a function component declares a piece of state it owns:
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}useState(0) initializes state to 0 and returns a pair: the current value (count) and a setter function (setCount) that updates it. The [value, setValue] naming convention comes from array destructuring — useState literally returns a two-element array — and every component using useState follows this same pattern.
Calling the setter triggers a re-render
Calling setCount does two things: it updates the value React has stored for this piece of state, and it schedules the component to re-render with the new value. This is the entire mechanism that makes the UI "react" to state — you never manually tell React to update the DOM, you just call the setter and let it re-render.
State updates don't happen immediately
setCount doesn't change count synchronously in the currently running function — it schedules the update for the next render:
function handleClick() {
setCount(count + 1);
console.log(count); // still the OLD value here
}React also batches multiple state updates triggered within the same event handler into a single re-render, for performance — so calling a setter multiple times in a row doesn't cause multiple renders.
Functional updates for reliable increments
Calling setCount(count + 1) twice in the same handler doesn't add 2, because both calls capture the same count from that render's closure:
function handleClick() {
setCount(count + 1);
setCount(count + 1); // still uses the same stale `count` — net effect: +1, not +2
}Pass a function instead when the new value depends on the previous one, and React guarantees it receives the latest state, not a stale snapshot:
function handleClick() {
setCount((prev) => prev + 1);
setCount((prev) => prev + 1); // now correctly +2
}Never mutate state directly
For objects and arrays, mutating in place doesn't trigger a re-render, because React compares the old and new values by reference to decide whether anything changed:
// Wrong — same array reference, React sees no change
todos.push(newTodo);
setTodos(todos);
// Right — a new array reference
setTodos([...todos, newTodo]);Always create a new object or array (with spread syntax, map, filter, etc.) rather than mutating the existing one.
The next lesson covers useEffect, for running code in response to a component rendering rather than in response to a specific event.