Runnable lesson · press Try it on the examples to edit and run them here.
Calling the setter is simple, but a few details decide whether your updates behave. This lesson covers the ones that trip up beginners.
Setting does not change the variable you already have
When you call setCount(5), React schedules a re-render. The count variable in the function that is already running does not change. The new value appears in the next render.
function App() {
const [count, setCount] = React.useState(0);
function addThree() {
setCount(count + 1); // count is 0 here, so this asks for 1
setCount(count + 1); // still 0, so asks for 1 again
setCount(count + 1); // still asks for 1
}
return (
<div>
<p>Count: {count}</p>
<button onClick={addThree}>Add three</button>
</div>
);
}
After clicking, the count is 1, not 3. All three calls used the same count (0), so they all asked for 1. The next lesson, State as a Snapshot, explains why.
The updater function
When the new value depends on the previous one, pass a function. React calls it with the latest pending value:
function App() {
const [count, setCount] = React.useState(0);
function addThree() {
setCount((c) => c + 1);
setCount((c) => c + 1);
setCount((c) => c + 1);
}
return (
<div>
<p>Count: {count}</p>
<button onClick={addThree}>Add three</button>
</div>
);
}
Now it adds 3. React queues the updater functions and applies them one after another, each receiving the result of the previous one. The convention is to name the parameter after the state (c for count, or prev).
Rule of thumb: if the next value is computed from the previous value, use the updater form. If it is a fixed value (setName("Ada")) or comes from an event (setText(e.target.value)), pass the value directly.
Updates are batched
React groups all the state updates from one event handler and re-renders once at the end, not after each call. This is called batching, and since React 18 it applies everywhere (event handlers, timeouts, promises). It makes the UI faster and avoids showing half-finished states.
function App() {
const [a, setA] = React.useState(0);
const [b, setB] = React.useState(0);
const [c, setC] = React.useState(0);
const renders = React.useRef(0);
renders.current += 1;
function update() {
setA(a + 1);
setB(b + 1);
setC(c + 1);
}
return (
<div>
<p>a={a} b={b} c={c}</p>
<p>Renders: {renders.current}</p>
<button onClick={update}>Update three things</button>
</div>
);
}
Three setters, but only one extra render: the counter goes from 1 to 2, not to 4. (The render counter uses a ref, which is explained in useRef.)
Setting the same value does nothing
If you set state to a value that is the same as the current one (compared with Object.is), React skips the re-render. That is why mutating an object and setting it again does not work: it is still the same object. See Avoiding State Mutation.
State is replaced, not merged
With useState, the setter replaces the whole value. For objects this differs from how classes worked:
function App() {
const [user, setUser] = React.useState({ name: "Ada", age: 36 });
function birthday() {
// Wrong: setUser({ age: user.age + 1 }) would lose the name
setUser({ ...user, age: user.age + 1 });
}
return (
<div>
<p>{user.name}, {user.age}</p>
<button onClick={birthday}>Birthday</button>
</div>
);
}
We copy the old fields with ...user and override age. Updating Objects in State goes further.
Updater functions must be pure
An updater function should only calculate the next value from the previous one. Do not call other setters or send requests inside it. React may call it twice in development to check this.
Key points
- Setting state schedules a render; the current variable keeps its old value.
- Use
setX(prev => ...)when the new value depends on the old one. - React batches updates into one render, and setters replace rather than merge.