- Home
- /
- Tutorials
- /
- React Tutorial
- /
- Updating State Correctly
State and Reducers
Updating State Correctly
Two rules cover nearly every state bug: never change the existing value, and never read the current value to compute the next one without the updater form.
Updates are batched
Calling the setter does not change the variable immediately. React records the request and re-renders once, so count keeps its value for the rest of the current render - and calling the setter three times with the same stale value only moves it once.
Three calls, one increment
jsx
function Batching() {
const [count, setCount] = React.useState(0)
function addThreeWrong() {
// All three read the same count from this render.
setCount(count + 1)
setCount(count + 1)
setCount(count + 1)
}
function addThreeRight() {
// Each receives the latest pending value.
setCount((current) => current + 1)
setCount((current) => current + 1)
setCount((current) => current + 1)
}
return (
<div>
<p>Count: {count}</p>
<button onClick={addThreeWrong}>Wrong (+1)</button>{" "}
<button onClick={addThreeRight}>Right (+3)</button>{" "}
<button onClick={() => setCount(0)}>Reset</button>
</div>
)
}
const root = ReactDOM.createRoot(document.getElementById("root"))
root.render(<Batching />)Press each button and watch the difference. The rule: if the next value depends on the current one, pass a function.
Never mutate
React decides whether to re-render by comparing the new value with the old one by identity. Pushing to an array gives back the same array, so React sees no change and does nothing at all.
Mutation is invisible to React
jsx
function Lists() {
const [broken, setBroken] = React.useState(["a"])
const [fixed, setFixed] = React.useState(["a"])
return (
<div>
<p>Mutated: {broken.join(", ")}</p>
<button onClick={() => { broken.push("new"); setBroken(broken) }}>
Push and set (no visible change)
</button>
<p style={{ marginTop: 12 }}>Replaced: {fixed.join(", ")}</p>
<button onClick={() => setFixed([...fixed, "new"])}>
Spread into a new array (works)
</button>
</div>
)
}
const root = ReactDOM.createRoot(document.getElementById("root"))
root.render(<Lists />)The first button really does grow its array - press it five times, then press the second button and the mutations appear all at once, because that render finally happened.
The operations you need
- Add -
[...items, next] - Remove -
items.filter((i) => i.id !== id) - Replace one -
items.map((i) => (i.id === id ? updated : i)) - Object field -
{ ...user, name: "Ada" }
Every one of those returns a new value. push, splice, sort and reverse all change the original, so sort a copy: [...items].sort().
Objects need the spread too
Updating one field
jsx
function Settings() {
const [user, setUser] = React.useState({ name: "Ada", theme: "light" })
return (
<div>
<p>{user.name} — {user.theme}</p>
<button onClick={() => setUser({ ...user, theme: user.theme === "light" ? "dark" : "light" })}>
Toggle theme
</button>
</div>
)
}
const root = ReactDOM.createRoot(document.getElementById("root"))
root.render(<Settings />)The spread copies the other fields so they are not lost. Passing { theme: "dark" } alone would replace the whole object and name would vanish.
