Runnable lesson · press Try it on the examples to edit and run them here.
State that is an object must be treated as read-only. To change it, create a new object with the changes and pass it to the setter.
Why not just change a property?
function App() {
const [user, setUser] = React.useState({ name: "Ada", city: "London" });
function wrongRename() {
user.name = "Grace"; // mutation: React cannot notice this
setUser(user); // same object, so React skips the re-render
}
return (
<div>
<p>{user.name} in {user.city}</p>
<button onClick={wrongRename}>Rename (wrong way)</button>
</div>
);
}
The name in the object changes, but the screen does not: setUser was given the same object, and React compares by identity. There are other bugs too: mutated objects break memoization and make updates unpredictable. The fix is always to create a copy.
The spread syntax
{ ...user } copies all properties into a new object. Properties written after it override:
function App() {
const [user, setUser] = React.useState({ name: "Ada", city: "London" });
function rename() {
setUser({ ...user, name: "Grace" });
}
return (
<div>
<p>{user.name} in {user.city}</p>
<button onClick={rename}>Rename</button>
</div>
);
}
See Object Spread if the syntax is new.
One handler for many fields
A computed property name lets one handler update any field:
function App() {
const [form, setForm] = React.useState({ name: "Ada", city: "London" });
function handleChange(e) {
setForm({ ...form, [e.target.name]: e.target.value });
}
return (
<div>
<label>Name: <input aria-label="name" name="name" value={form.name} onChange={handleChange} /></label>
<label>City: <input aria-label="city" name="city" value={form.city} onChange={handleChange} /></label>
<p>{form.name} lives in {form.city}</p>
</div>
);
}
Nested objects
The spread copy is shallow: nested objects are still shared. To change something deep, copy every level on the path:
function App() {
const [user, setUser] = React.useState({
name: "Ada",
address: { city: "London", zip: "N1" },
});
function move() {
setUser({
...user, // copy the user
address: { ...user.address, city: "Paris" } // copy address and change the city
});
}
return (
<div>
<p>{user.name}, {user.address.city}</p>
<button onClick={move}>Move to Paris</button>
</div>
);
}
The old user and its address stay untouched; the new ones share the unchanged parts.
When the nesting gets deep
Deeply nested state is hard to update. Two good options:
- Flatten the state. Store
{ name, city, zip }or keep related data in separate states and use ids to link them. - Use a helper library such as Immer, which lets you write “mutating” code on a draft while producing a new immutable object (the
use-immerpackage gives youuseImmer). It is optional; the plain spread approach is enough for most apps.
Updating based on previous state
If your update depends on the previous object, use the updater function so you always start from the latest value:
function App() {
const [game, setGame] = React.useState({ player: "Ada", score: 0 });
return (
<div>
<p>Score: {game.score}</p>
<button onClick={() => setGame((g) => ({ ...g, score: g.score + 1 }))}>Add point</button>
</div>
);
}
Remember the parentheses around the object literal in an arrow function: (g) => ({ ... }). Without them the braces are read as a function body.
Key points
- Never assign to a property of state; always set a new object.
- Copy with
{ ...old, changed }and copy each nested level you change. - Flatten deep state or use Immer when updates get awkward.