Skip to main content

State and Reducers

Structuring State

Written by Published

Most state bugs are structural. If a value can be calculated from another value, storing it means two things that can disagree.

Do not store what you can derive

A value computed from existing state does not need its own state. Compute it during render - it is free, and it can never fall out of sync.

Derived, not stored

jsx

function Cart() {
  const [items, setItems] = React.useState([
    { name: "Book", price: 12 },
    { name: "Pen", price: 3 },
  ])

  // Derived on every render. No second state, nothing to keep in sync.
  const total = items.reduce((sum, item) => sum + item.price, 0)
  const isEmpty = items.length === 0

  return (
    <div>
      <p>{isEmpty ? "Cart is empty" : `${items.length} items, total ${total}`}</p>
      <button onClick={() => setItems([...items, { name: "Mug", price: 8 }])}>Add mug</button>{" "}
      <button onClick={() => setItems([])}>Empty</button>
    </div>
  )
}

const root = ReactDOM.createRoot(document.getElementById("root"))
root.render(<Cart />)

Had total been its own state, every place that changes items would also have to remember to update it. Forget once and the number is wrong.

Separate or together?

Use separate state when values change independently. Group them into an object when they always change at the same moment.

  • Separate - a search box and a sort order. Changing one should not touch the other.
  • Together - the x and y of a dragged element. They always move as a pair.

When in doubt, start separate. Splitting an object later is easy; untangling two values that turned out to be independent is not.

Avoid impossible combinations

Three booleans give eight combinations, and usually only three are meaningful. isLoading and isError both true is a state your UI has no answer for - but nothing stops it happening.

One value instead of three flags

jsx

function Loader() {
  // "idle" | "loading" | "success" | "error" — no impossible combinations.
  const [status, setStatus] = React.useState("idle")

  return (
    <div>
      <p>Status: <strong>{status}</strong></p>
      {status === "loading" && <p>Working…</p>}
      {status === "success" && <p style={{ color: "#15803d" }}>Done.</p>}
      {status === "error" && <p style={{ color: "#b91c1c" }}>Something failed.</p>}

      <div style={{ display: "flex", gap: 6, marginTop: 8 }}>
        {["idle", "loading", "success", "error"].map((next) => (
          <button key={next} onClick={() => setStatus(next)}>{next}</button>
        ))}
      </div>
    </div>
  )
}

const root = ReactDOM.createRoot(document.getElementById("root"))
root.render(<Loader />)

Do not copy props into state

Putting a prop into useState takes a snapshot. When the prop changes the state keeps the old value, and the component quietly shows stale data - the trap from the previous chapter.

Only do it when you genuinely want an initial value the user then edits, and name it so that intent is obvious: initialName rather than name.