State Updates and Renders
Calling a state setter requests another render. It does not change the state variable inside the handler that is already running.
function handleClick() {
setCount(count + 1);
console.log(count);
}If the button displayed zero before the click, the log prints zero. That render created both the count binding and the handler.
The next render creates another count binding with the next state. React does not rewrite the local variable in the completed render.
Every Render Receives a State Snapshot
Consider the first two renders of Counter.
render 1 receives count 0
render 1 creates a handler that sees count 0
click calls that handler
handler requests count 1
render 2 receives count 1
render 2 creates a handler that sees count 1Each handler closes over the variables from its own component call. A handler from render one continues to see render one's values even after React has produced render two.
This becomes visible after a delay.
function handleClick() {
setCount(count + 1);
setTimeout(() => {
console.log(count);
}, 1000);
}The timeout callback closes over the same count as the click handler. It prints that snapshot one second later.
State variables are fixed for one render. A setter queues state for a later render.
Do not use a timeout merely to wait for state to change. Put UI calculation in the next render, and put follow-on user work in the same event when it already has the needed next value.
React Processes Updates After the Handler
React batches state updates during an interaction. It waits until the event handler has finished before processing the queued work and committing the next screen.
function handleSave() {
setSaved(true);
setMessage("Goal saved");
}React can process both requests together and render one consistent next result. The user does not need an intermediate screen with saved changed but an old message.
Batching also means the DOM is not updated between those two setter calls.
function handleSave() {
setSaved(true);
console.log(statusElement.textContent);
}The DOM read can still show the previous committed text. React has not committed the requested state yet.
Most application code should not read React-managed DOM immediately after a setter. Calculate UI from state and let the next commit update the document.
Batching groups compatible state requests before rendering. It does not combine the state variables into one value.
Repeated Value Updates Use One Snapshot
This handler calls the setter three times.
function handleAddThree() {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
}All three expressions read the same count from the current render. If it is zero, each call becomes setCount(1).
request replace with 1
request replace with 1
request replace with 1The next state is one, not three.
The calls did not execute a live increment operation against mutable React state. They each calculated a replacement value from one snapshot.
Updater Functions Use the Queued Value
Pass a function when the next state depends on the state already in the update queue.
function handleAddThree() {
setCount((current) => current + 1);
setCount((current) => current + 1);
setCount((current) => current + 1);
}React queues the functions. During update processing, the result from one becomes the input to the next.
start with 0
first updater receives 0 and returns 1
second updater receives 1 and returns 2
third updater receives 2 and returns 3The next render receives three.
The parameter name is local to each updater.
setCount((previousCount) => previousCount + 1);current, previous, and value are ordinary names. Choose one that identifies the state being updated.
Use an updater function when the next value is calculated from the previous value.
A direct value remains clear when the next state does not depend on the previous state.
setStatus("complete");
setQuery("");Replacement Values and Updaters Share One Queue
React processes updates in call order.
setCount(5);
setCount((current) => current + 1);The replacement establishes five for the queued result. The updater receives five and returns six.
Reverse the order.
setCount((current) => current + 1);
setCount(5);The final replacement requests five, so the next state is five.
One handler rarely needs a long mix of replacements and updaters. When it does, keep the queue short enough to read in call order and add a test for the resulting interaction later.
Updater Functions Must Be Pure
An updater receives pending state and returns next state. It must not change the pending value or perform unrelated work.
setLesson((current) => {
current.complete = true;
return current;
});This mutates the state object and returns the same reference.
Return another object.
setLesson((current) => ({
...current,
complete: true,
}));Development Strict Mode can call updater functions twice and ignore one result to expose impurities. A pure updater can run again with the same input without changing external data.
Do not log analytics, write storage, or send a request inside an updater. Put user-event work in the event handler and let the updater calculate only the next state value.
Replace State Objects Instead of Mutating Them
State can hold an object.
const [lesson, setLesson] = useState({
title: "State Updates",
complete: false,
});This mutation changes the existing object.
lesson.complete = true;
setLesson(lesson);The state reference passed to the setter is the same reference React already holds. React compares state values with Object.is, so it can treat the request as unchanged. The previous state was also modified, removing the old snapshot needed for predictable rendering and debugging.
Create another object.
setLesson((current) => ({
...current,
complete: true,
}));Object spread copies the top level and the later property supplies the changed value. The copy is shallow, so nested changes require copying each changed level.
setLesson((current) => ({
...current,
author: {
...current.author,
name: "Ishtmeet",
},
}));State objects and arrays are mutable JavaScript values, but React update code should treat stored snapshots as read-only.
Replace State Arrays with New Arrays
Add one lesson without changing the existing array.
setLessons((current) => [
...current,
newLesson,
]);Remove one through filter.
setLessons((current) =>
current.filter((lesson) => lesson.id !== lessonId),
);Update one record through map.
setLessons((current) =>
current.map((lesson) =>
lesson.id === lessonId
? { ...lesson, complete: true }
: lesson,
),
);Unchanged records keep their existing object references. The selected record receives a new object. The array itself is new.
Avoid push, pop, splice, sort, and reverse on the current state array. Use non-mutating operations such as map, filter, toSorted, and spread.
Same-Value State Can Be Skipped
React uses Object.is to compare the next state with the current state.
setCount(0);If the state is already zero, React can skip work for that update. No new visible value exists to commit.
For objects and arrays, Object.is compares references.
Object.is({}, {}); // false
Object.is(lesson, lesson); // trueCreating a new object for a real state change provides a new reference. Creating a new object with identical fields on every event still requests another value and can produce unnecessary rendering.
Do not add deep equality checks inside every setter. Create a new state value when the stored information changes.
React can still call a component before deciding that a same-value update can be discarded in selected cases. Application correctness must depend on pure rendering, not on a promise that the function is never called.
Trace Render and Commit After State
A state update supplies a concrete trigger for another render.
button click
-> handler reads its snapshot
-> setter queues next state
-> handler finishes
-> React renders affected components
-> React compares next output with current output
-> React DOM commits required host changes
-> browser paintsThe component function runs during render. A changed state value does not imply every returned DOM node changes during commit.
function Counter() {
const [count, setCount] = useState(0);
return (
<section>
<h2>Lesson counter</h2>
<button onClick={() => setCount((n) => n + 1)}>
{count}
</button>
</section>
);
}The heading output remains the same. React DOM can retain its host text while updating the button text.
Strict Mode Exposes Render-Time Mutations
The Vite entry wraps the app in StrictMode during development.
<StrictMode>
<App />
</StrictMode>React can call component functions, state initializers, and updater functions again during development checks. One result is discarded.
This component mutation produces a different array length on repeated calls.
function LessonList({ lessons }) {
lessons.push({ id: "extra", title: "Extra" });
return <p>{lessons.length}</p>;
}Remove the mutation and calculate from current inputs.
Strict Mode does not call the user event handler twice. The button click occurs once. The updater queued by that handler can be called again to verify its purity.
Do not remove Strict Mode to hide repeated development logs. Locate render-time or updater-time work that should be pure.
State Setters Keep a Stable Identity
React returns the same setter function for a state position across renders.
const [count, setCount] = useState(0);The local count binding changes with state snapshots. The setCount function identity remains stable.
This lets a component pass the setter through a callback prop when that public contract is appropriate, though named action callbacks often communicate intent better.
<CounterControls onIncrement={() => setCount((n) => n + 1)} />State setters have stable identity. No Effect is needed for this counter.
Debug One Update
Add logs around the setter.
function handleClick() {
console.log("handler snapshot", count);
setCount((current) => current + 1);
}
console.log("render snapshot", count);Click once in development. Strict Mode can produce more render logs than commits, but one click produces one handler log. The next committed screen displays the updater result.
React DevTools can show the current state after the render. The browser Elements panel can show the committed text. Use the logs only to connect handler timing, then remove them.
Lesson Check
The update model now supports exact predictions.
state is fixed within one render
setters queue values or updater functions
React batches compatible requests
updaters process in order from queued state
objects and arrays receive new references for changes
render calculates the next tree
commit applies only required host workSource Notes
- React documentation, State as a Snapshot
- React documentation, Queueing a Series of State Updates
- React documentation, Updating Objects in State
- React documentation, Updating Arrays in State
- React reference,
useState