Repeats one replacement
Three calls to setCount(count + 1) all calculate from the same snapshot, so they each request the same value.
Setting state does not change the current render. It asks React to calculate a later render, and React processes queued updates in a predictable order.
Every render receives a snapshot of state. Calling a setter such as setScore queues an update, but the score variable in the running event handler remains the value from that render.
0 because this handler closes over the current render's snapshot.import { useState } from "react";
function Scoreboard() {
const [score, setScore] = useState(0);
function handleClick() {
setScore(score + 1);
console.log(score); // 0, not 1
}
return <button onClick={handleClick}>
Score: {score}
</button>;
}
A setter is a request, not an assignment. If later code in the same handler needs the next value, calculate it in a local variable or use a functional update when it depends on queued state.
React waits until the event handler has finished before processing its state updates. This batching avoids an extra render after every setter call and prevents the screen from showing half-finished state.
function CheckoutButton() {
const [status, setStatus] = useState("idle");
const [message, setMessage] = useState("");
function handleCheckout() {
setStatus("submitting");
setMessage("Processing order");
}
return (
<button onClick={handleCheckout}>
{status === "idle" ? "Checkout" : message}
</button>
);
}
React does not batch separate intentional interactions into one event. A second click gets its own handler and its own state snapshot. Modern React also batches many updates from promises, timers, and native event handlers when the app uses createRoot.
When the next value depends on the previous value, pass an updater function to the setter. React puts that function in the queue. During the next render, it calls each updater in order and passes the result to the next one.
Three calls to setCount(count + 1) all calculate from the same snapshot, so they each request the same value.
Three calls to setCount(c => c + 1) receive 0, then 1, then 2, producing 3.
function Counter() {
const [count, setCount] = useState(0);
function addThree() {
setCount(current => current + 1);
setCount(current => current + 1);
setCount(current => current + 1);
}
return <button onClick={addThree}>Count: {count}</button>;
}
Updater functions run while React renders, so keep them pure. Return the next state without changing existing objects, arrays, variables outside the function, or the DOM. In development, Strict Mode may call an updater twice to reveal accidental mutation.
Treat objects stored in state as read-only. Mutating an existing object keeps the same reference and changes data that older render snapshots may still rely on. Instead, create a new object and copy the fields that should remain.
city property replaces only that field.function ProfileEditor() {
const [profile, setProfile] = useState({
name: "Mira",
city: "Pune",
});
function handleCityChange(event) {
setProfile(current => ({
...current,
city: event.target.value,
}));
}
return (
<input value={profile.city} onChange={handleCityChange} />
);
}
Spread syntax is shallow. To change a nested value, copy every object from the changed field back to the top-level state object.
settings and its nested notifications object receive new references.setSettings(current => ({
...current,
notifications: {
...current.notifications,
email: false,
},
}));
Do not write profile.city = value. Build a new object and pass it to the setter. When state becomes deeply nested and awkward to copy, consider flattening its shape or using a reducer.
Arrays in state are objects too, so treat them as read-only. Create a new array with spread syntax, map(), or filter(), then store that array.
Use [...items, newItem] instead of items.push(newItem).
filter().Keep every item whose ID is not the one being removed.
map().Return an updated copy for the matching item and return every other item unchanged.
function TaskList() {
const [tasks, setTasks] = useState([
{ id: 1, title: "Read the state notes", done: false },
]);
function addTask(task) {
setTasks(current => [...current, task]);
}
function toggleTask(taskId) {
setTasks(current =>
current.map(task =>
task.id === taskId ? { ...task, done: !task.done } : task
)
);
}
function removeTask(taskId) {
setTasks(current => current.filter(task => task.id !== taskId));
}
return (
<ul>
{tasks.map(task => (
<li key={task.id}>
<button onClick={() => toggleTask(task.id)}>
{task.done ? "Undo" : "Done"}
</button>
{task.title}
<button onClick={() => removeTask(task.id)}>
Remove
</button>
</li>
))}
</ul>
);
}
Methods such as push(), pop(), splice(), sort(), and reverse() mutate the original array. For sorting or reversing, copy first: const next = [...items]; next.sort(compareItems); setItems(next);.
The running handler belongs to the current render and sees that render's fixed state snapshot. The setter queues work for a later render rather than changing the local variable.
setCount(current => current + 1)?
Use a functional update when the next state depends on previous or already queued state. React passes each updater the latest pending value.
Use map() to create a new array. For the matching item, return a new object with the changed field; return every other item unchanged.