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 useStatecalls 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 useStatecalls 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.
- useStatereplaces 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 useStatecalls?
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.