React - useState (Patterns)
Goal
Learn practical patterns for useState
beyond simple counters: managing objects and arrays, using multiple state variables, and lazy initialization.
What you’ll learn
- Update object and array state immutably (no mutation).
- Split state into multiple
useState
calls when it improves clarity. - Use lazy initialization for expensive initial state.
Try it
View source
const { useState } = React;
// 1) Object state (profile editor)
function ProfileEditor(){
const [profile, setProfile] = useState({ name: "Ada", email: "[email protected]" });
function updateField(key, value){
// Replace, don’t mutate: setProfile({ ...profile, [key]: value })
setProfile(p => ({ ...p, [key]: value }));
}
return (
<div>
<h4>Profile</h4>
<input value={profile.name} onChange={e => updateField('name', e.target.value)} placeholder="Name" />
<input value={profile.email} onChange={e => updateField('email', e.target.value)} placeholder="Email" style={{marginLeft:8}} />
<p style={{marginTop:8}}>Preview: {profile.name} ({profile.email})</p>
</div>
);
}
// 2) Array state (todo list)
function TodoList(){
const [text, setText] = useState("");
const [items, setItems] = useState(["Learn useState", "Practice patterns"]);
function addItem(){
if (!text.trim()) return;
setItems(arr => [...arr, text.trim()]); // append
setText("");
}
function removeIndex(i){
setItems(arr => arr.filter((_, idx) => idx !== i));
}
return (
<div>
<h4>Todos</h4>
<input value={text} onChange={e => setText(e.target.value)} placeholder="New todo" />
<button onClick={addItem} style={{marginLeft:8}}>Add</button>
<ul>
{items.map((it, i) => (
<li key={i}>{it} <button onClick={() => removeIndex(i)} style={{marginLeft:8}}>✕</button></li>
))}
</ul>
</div>
);
}
// 3) Multiple state variables + Lazy initialization
function ExpensiveInitDemo(){
// Lazy init runs once (the function returns the initial value)
const [seed] = useState(() => {
// Simulate expensive work
const n = Math.floor(Math.random()*1000);
return n;
});
const [a, setA] = useState(seed);
const [b, setB] = useState(0);
return (
<div>
<h4>Lazy init + Multiple states</h4>
<p>Seed: {seed} | a: {a} | b: {b}</p>
<button onClick={() => setA(x => x + 1)}>Inc a</button>
<button onClick={() => setB(x => x + 1)} style={{marginLeft:8}}>Inc b</button>
</div>
);
}
const root = ReactDOM.createRoot(document.getElementById('try-usestate-patterns'));
root.render(<>
<ProfileEditor />
<hr />
<TodoList />
<hr />
<ExpensiveInitDemo />
</>);
Patterns
1) Object state
- Replace, don’t mutate:
setUser(u => ({ ...u, name: "Ada" }))
- Spread (
...
) copies existing fields and overrides changed fields.
2) Array state
- Add:
setList(a => [...a, item])
- Remove:
setList(a => a.filter(x => x.id !== id))
- Update:
setList(a => a.map(x => x.id === id ? { ...x, done: true } : x))
3) Multiple state variables
- Prefer several
useState
calls when pieces change independently. - Group them into an object only if they’re tightly coupled (updated together).
4) Lazy initialization
- Use
useState(() => computeInitial())
to run expensive initialization once.
Common pitfalls
- Direct mutation won’t re-render:
state.push(...)
,state.name = ...
— avoid; always set a new value. useState
replaces the value; it does not merge objects for you.- Avoid duplicating derived state; compute it from existing state/props when possible.
Checks for Understanding
- Why use functional updates like
setX(x => x + 1)
? - When should you split state into multiple
useState
calls?
Show answers
- They avoid stale reads when multiple updates are batched or triggered rapidly.
- When pieces of state change independently; it improves clarity and avoids accidental coupling.
Exercises
- Extend the todo list to support editing existing items without mutating state.
- Extract the profile editor into separate child components and lift state appropriately.
- Add a computed “completed count” derived from the todo array rather than storing it.