State is what makes a UI interactive; misunderstanding when it updates is the most common React bug.
Regular variables don't survive re-renders, and changing them doesn't tell React to update. For data that changes over time, use state:
const [count, setCount] = useState(0);useState returns the current value and a setter. Calling the setter stores the new value and schedules a re-render; on that render useState returns the new value. Each component instance has its own state: render <Counter /> twice and you get two independent counts.
useState is a Hook. Hooks are functions starting with use, and they must be called at the top level of a component (not inside conditions, loops or nested functions), because React matches them up by call order.
The key mental model: state is a snapshot. During one render, count is a constant. Setting it doesn't change the variable you already have, it requests a new render:
setCount(count + 1);
setCount(count + 1); // still count + 1: both read the same snapshot
console.log(count); // old valueReact also batches updates from the same event into one re-render. When the next value depends on the previous one, pass an updater function, which React applies to the latest queued state:
setCount(c => c + 1);
setCount(c => c + 1); // +2If initial state is expensive to compute, pass a function: useState(() => buildIndex(data)) runs only on the first render.
Only store what can't be computed. If a value can be derived from props or other state (a full name from first and last, a filtered list), compute it during render instead of keeping it in sync in state.
Example
import { useState } from "react";
function Counter({ step = 1 }) {
const [count, setCount] = useState(0);
function addThree() {
// updater functions see the latest pending value
setCount(c => c + step);
setCount(c => c + step);
setCount(c => c + step);
console.log(count); // still the old snapshot
}
const parity = count % 2 === 0 ? "even" : "odd"; // derived, not state
return (
<div>
<p>{count} ({parity})</p>
<button onClick={() => setCount(count + step)}>+{step}</button>
<button onClick={addThree}>+{3 * step}</button>
<button onClick={() => setCount(0)}>Reset</button>
</div>
);
}
export default function App() {
return <><Counter /><Counter step={10} /></>;
}Remember
const [v, setV] = useState(init); calling the setter schedules a re-render- State is a snapshot: the variable doesn't change until the next render
- Use
setV(prev => next)when the new value depends on the old one - Call Hooks only at the top level; derive values instead of duplicating them in state
Watch out for
- Reading state right after setting it and expecting the new value.
- Copying props into state and then wondering why it doesn't update when the prop changes.