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

  1. Why use functional updates like setX(x => x + 1)?
  2. When should you split state into multiple useState calls?
Show answers
  1. They avoid stale reads when multiple updates are batched or triggered rapidly.
  2. When pieces of state change independently; it improves clarity and avoids accidental coupling.

Exercises

  1. Extend the todo list to support editing existing items without mutating state.
  2. Extract the profile editor into separate child components and lift state appropriately.
  3. Add a computed “completed count” derived from the todo array rather than storing it.