State Updates and Renders
What actually happens when you call a React state setter?
I think the first assumption almost everyone makes is that React changes the variable right there. You call setCount(count + 1), so obviously count should now be one higher, right?
Well... try this.
function handleClick() {
setCount(count + 1);
console.log(count);
}Say the button currently shows 0. You click it, and the console logs 0.
Which can be slightly annoying the first time you see it. We literally just asked React to make it 1, why are we still getting 0?
Because setCount() doesn't rewrite the count variable inside the function that's already running. It requests another render with new state. The current handler keeps the values from the render that created it.
That idea comes up everywhere with React state, so let's spend some time on it.
Every render gets its own state values
Suppose our component renders for the first time and count is 0.
During that render, React calls the component function, gives it count = 0, and the component creates a click handler which also sees that same count = 0.
Then we click the button. That exact handler runs and calls:
setCount(count + 1);Since its count is 0, it requests 1.
React later renders the component again, and this new call gets count = 1. It also creates a new handler, and that new handler sees 1.
So if we write the sequence out:
render 1 gets count 0
render 1 creates a handler which sees count 0
click runs that handler
handler requests count 1
render 2 gets count 1
render 2 creates a handler which sees count 1React doesn't go back into render one and somehow replace its local count variable with 1. That function call already happened.
And JavaScript closures still work normally here. A callback keeps access to values from the function call where it was created.
You can see this very clearly with a timeout:
function handleClick() {
setCount(count + 1);
setTimeout(() => {
console.log(count);
}, 1000);
}Suppose count was 0 when you clicked.
One second later that callback still prints 0, even if the screen already shows 1.
Why? The timeout callback was created by the handler from the render where count was 0. It still has access to that render's value.
React docs call state a snapshot, and that's actually a pretty useful word for this. For one component render, the state values you got are fixed.
Calling a setter asks React to create another render with another set of values.
A state variable doesn't change halfway through the render or event handler which already has it. Calling its setter requests state for a later render.
And please don't fix this by adding setTimeout() just so you can "wait for React state to update". That's usually a sign the code is depending on the wrong value.
If you already know the next value inside the handler, just calculate it there.
function handleClick() {
const nextCount = count + 1;
setCount(nextCount);
console.log(nextCount);
}Now you're logging the value you calculated, not expecting React to rewrite your existing variable.
React doesn't immediately render after every setter line
Now say a handler changes two pieces of state:
function handleSave() {
setSaved(true);
setMessage("Goal saved");
}React can process these together.
It generally waits until your event handler finishes, then processes the queued updates and renders the next UI.
This is called batching.
Without batching, you could potentially get one render after setSaved(true) and then another after setMessage("Goal saved"). React can instead process the compatible updates together and commit the resulting UI.
The same thing also explains why reading the DOM immediately after a state setter can still give you the old DOM.
function handleSave() {
setSaved(true);
console.log(statusElement.textContent);
}That setter requested another render. React hasn't necessarily committed the new DOM while this same handler is still running, so textContent can still contain whatever was on screen before the click.
Normally you shouldn't need to manually inspect React-managed DOM after setting state anyway. Your component should calculate the UI from state, and React DOM updates the actual elements after React renders.
Also, batching doesn't mean React merges all your state variables into one giant state value. Each state position is still separate. React is just processing multiple update requests together before rendering.
Why three setCount(count + 1) calls only give you one
This example looks strange until you apply the snapshot rule:
function handleAddThree() {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
}Surely we're adding three?
Nope.
Let's say count is 0 for this render. Every line reads the same count.
So React effectively receives:
setCount(1);
setCount(1);
setCount(1);All three lines calculated their value from count = 0.
There was no live count variable changing from zero to one, then one to two, then two to three while the handler ran.
You requested replacement state three times, and every request happened to request the same value.
So the resulting count is 1.
This is one of those React examples people memorize without understanding, and then it comes back later in less obvious code. Don't memorize the output. Follow which render the value came from.
Updater functions are different
Now change the code to this:
function handleAddThree() {
setCount((current) => current + 1);
setCount((current) => current + 1);
setCount((current) => current + 1);
}Now the result is 3.
What's different?
We're no longer calculating the next value ourselves from the count variable in the handler. We're giving React functions.
React queues those updater functions, and when it processes them, each updater receives the state produced by the previous update.
So starting from zero:
first updater gets 0, returns 1
second updater gets 1, returns 2
third updater gets 2, returns 3Then the next render receives 3.
The parameter name isn't special, by the way.
This:
setCount((current) => current + 1);and this:
setCount((previousCount) => previousCount + 1);mean the same thing.
Use whatever name makes the code easiest to read.
The useful rule here is pretty simple: if your next state is calculated from the previous state, use the updater form.
setCount((count) => count + 1);If the new value doesn't depend on the old value, just pass the value.
setStatus("complete");
setQuery("");There's no reason to write:
setStatus(() => "complete");unless you somehow enjoy making simple code look suspicious.
Direct values and updater functions go into the same queue
You can also mix them.
For example:
setCount(5);
setCount((current) => current + 1);React processes these in order.
First we request replacement state 5. Then the updater receives 5 and returns 6.
So the next state is 6.
Now reverse them:
setCount((current) => current + 1);
setCount(5);The updater runs first, but then the direct replacement says the resulting state should be 5.
So you end up with 5.
You probably won't write long chains mixing these two forms very often. If you do, read them from top to bottom and follow the state value through each queued update. Once there's six different setters mixed together, maybe the handler wants simplifying anyway.
Updater functions should only calculate state
Since React can run updater functions while it's processing queued state, those functions need to behave predictably.
Don't do this:
setLesson((current) => {
current.complete = true;
return current;
});There are two problems here.
We're changing the existing object, and then we're returning that exact same object reference.
Write this instead:
setLesson((current) => ({
...current,
complete: true,
}));Now we're returning another object containing the updated data.
Also don't put unrelated work inside an updater:
setCount((current) => {
saveCountToServer(current + 1);
return current + 1;
});That's bad code because the updater now does more than calculate state. React expects updater functions to be pure, and development checks can call them again.
Do the event work in the event handler and let the updater calculate the value.
function handleClick() {
setCount((current) => current + 1);
}If saving needs to happen because of the user action, handle that separately based on what the application needs.
Objects in state should be replaced, not edited
State doesn't have to be a number or string. You can store objects too.
const [lesson, setLesson] = useState({
title: "State Updates",
complete: false,
});Now you might be tempted to do:
lesson.complete = true;
setLesson(lesson);JavaScript allows it. The object itself is mutable.
But React code shouldn't update stored state that way.
The lesson object here came from an existing render. By changing lesson.complete directly, you've modified the same object React was already using for that state.
Then setLesson(lesson) passes React the same reference again.
React uses Object.is() when comparing state values, so React can see the same object reference and decide there isn't a new state value to process.
And there's another problem too: you've changed an older render's state object after that render happened.
That makes debugging much harder because the value you thought belonged to an older render has now been edited later.
Instead, create another object:
setLesson((current) => ({
...current,
complete: true,
}));The spread copies the existing top-level fields, then complete: true replaces that property in the new object.
If your object contains nested objects, spread is only a shallow copy.
Say the state looks something like this:
{
title: "State Updates",
author: {
name: "Ish",
verified: false
}
}If you're changing author.name, you need another object at that level too:
setLesson((current) => ({
...current,
author: {
...current.author,
name: "Ishtmeet",
},
}));You're creating a new outer object and a new author object, while values that didn't change can stay as they were.
JavaScript objects and arrays can be mutated, but when they're stored in React state, treat the stored value from that render as read-only.
Arrays follow the same rule
Suppose state contains lessons:
const [lessons, setLessons] = useState([]);To add a lesson, create another array:
setLessons((current) => [
...current,
newLesson,
]);To remove one:
setLessons((current) =>
current.filter((lesson) => lesson.id !== lessonId),
);And to update one item:
setLessons((current) =>
current.map((lesson) =>
lesson.id === lessonId
? { ...lesson, complete: true }
: lesson,
),
);Notice what happens in that map().
We create a new array. The lesson being changed gets a new object. Lessons that didn't change can keep their existing references.
That's exactly what we want.
Methods such as push(), pop(), splice(), sort(), and reverse() change the existing array, so don't call them directly on the current state array.
Use operations which return another array, such as map(), filter(), toSorted(), or spread syntax.
Setting the same value can result in no update
React compares the requested next state with the current state using Object.is().
So:
setCount(0);when count is already 0 doesn't give React any new state.
There may be nothing to update.
With objects, reference identity comes into it:
Object.is({}, {}); // false
const lesson = {};
Object.is(lesson, lesson); // trueTwo separately created objects are two different references, even if both currently contain the exact same fields.
That's why creating a new object tells React you have another state value.
But don't take this to the other extreme and create fresh objects for no reason every time some unrelated event happens.
If the stored information didn't change, returning the existing state is completely fine.
setLesson((current) => {
if (current.complete) {
return current;
}
return {
...current,
complete: true,
};
});And don't start adding deep-equality checks inside every setter either. Usually your update logic already knows whether anything changed.
One more small detail: React can sometimes call your component before it decides that a same-value update doesn't need further work. So application behavior shouldn't depend on the assumption that a component function will never run when the state eventually stays the same.
Keep render code pure and this isn't a problem.
What happens after state is queued?
Let's put the whole update sequence together.
Say the user clicks this button:
function Counter() {
const [count, setCount] = useState(0);
return (
<section>
<h2>Lesson counter</h2>
<button onClick={() => setCount((n) => n + 1)}>
{count}
</button>
</section>
);
}The click handler runs with the state values from its render. setCount() queues an updater. The handler finishes. React processes the update, calls the component again with the next count, and calculates the next React output.
Then React DOM checks what actually changed in the browser DOM and applies those changes.
In this example, the heading:
<h2>Lesson counter</h2>still says the exact same thing.
React doesn't need to remove it and create it again just because Counter() ran again.
The button text did change, so React DOM updates that part.
You can describe the sequence as:
button click
-> handler reads values from its render
-> setter queues an update
-> handler finishes
-> React calculates the next component output
-> React DOM applies required DOM changes
-> browser displays the updated resultA state update can cause a component render without every DOM element returned by that component being changed.
Remember from the previous chapter: React rendering and browser DOM mutation aren't the same operation.
Strict Mode makes bad updater code easier to notice
The Vite React setup normally has something along these lines:
<StrictMode>
<App />
</StrictMode>In development, Strict Mode can intentionally repeat some render-related work.
This confuses people because they add a console.log() to a component and suddenly see it twice.
And then the first instinct is sometimes, "React is broken, remove Strict Mode."
Please don't.
React is trying to expose code that behaves differently when run more than once.
For example, this component changes its input:
function LessonList({ lessons }) {
lessons.push({ id: "extra", title: "Extra" });
return <p>{lessons.length}</p>;
}Every time the component runs, it pushes another item into the same array.
Run it twice and now you've changed the result twice.
That's exactly the sort of mutation Strict Mode can make easier to spot.
The component should calculate output without editing incoming data.
Updater functions can also be called again by development checks, so this:
setCount((current) => current + 1);is safe because calling it with the same input returns the same output and doesn't modify anything elsewhere.
Strict Mode does not mean one user click calls your click handler twice.
One click still runs one event handler. React may repeat certain render or updater calculations afterward during development checks.
If Strict Mode exposes repeated logs or weird duplicated data, don't remove Strict Mode just to make the symptom disappear. Check whether render code or an updater is changing something outside itself.
State setters themselves stay stable
When you write:
const [count, setCount] = useState(0);the count binding can be different on every render.
Render one might have count = 0, render two might have count = 1, and so on.
But React keeps the setCount function identity stable for that state position.
So passing a setter or a callback which uses that setter is completely normal.
<CounterControls
onIncrement={() => setCount((n) => n + 1)}
/>Usually I prefer passing callbacks named after what the user can actually do, such as onIncrement, instead of exposing a raw setter to another component. It says more about what that child is allowed to request.
And none of this counter logic needs an Effect.
User clicks button, handler queues state, React renders. Done.
Don't add an Effect just because state changed.
Debugging one state update
If state timing still feels confusing, add two temporary logs.
function handleClick() {
console.log("handler snapshot", count);
setCount((current) => current + 1);
}
console.log("render snapshot", count);Now click once.
The handler log tells you which value the click handler received from its render. The render log tells you which value the component sees whenever React calls it.
With Strict Mode enabled in development, you might get more render logs than you expected. That's okay. The click handler itself still ran once for one click.
Then open React DevTools and check the current component state. Open the browser Elements panel and check the DOM text React DOM actually committed.
After you've understood the sequence, delete the logs. Otherwise six chapters later you'll open the console and wonder why your app apparently has opinions about "handler snapshot".
Check if you can predict these now
Before running each example, try saying what happens.
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);All three lines use count from the same render, so if count was zero, the next state is one.
Now:
setCount((n) => n + 1);
setCount((n) => n + 1);
setCount((n) => n + 1);Each updater receives the queued result from the previous updater, so zero becomes three.
And:
setCount(5);
setCount((n) => n + 1);becomes six.
While:
setCount((n) => n + 1);
setCount(5);ends at five.
If those results make sense without memorizing them, then you've got the useful part of React's state model.
State values belong to a particular render. Setters queue later state. Updater functions receive queued state in order. Objects and arrays should be replaced when their stored data changes. React then renders using the resulting state, and React DOM only changes the browser nodes that actually need updating.
That's pretty much the entire mechanism we need for the next few sections.