Derive a running total during render instead of storing it

The problem

Storing a total beside the items that produce it creates two sources of truth, and every mutation path — add, remove, edit quantity — has to remember to update the second one.

Short answer

Delete the total state and compute it in render from the items array; reach for useMemo only when measurement shows that pass is the cost.

The bug is visible in the state declaration, not in the arithmetic.

Language: JSX
import { useState } from 'react';

export function Cart({ initialItems = [] }) {
  const [items, setItems] = useState(initialItems);

  // No total state, no effect, no synchronisation problem.
  const total = items.reduce((sum, item) => sum + item.price * item.quantity, 0);

  function setQuantity(id, quantity) {
    setItems((current) =>
      current.map((item) => (item.id === id ? { ...item, quantity } : item)),
    );
  }

  function remove(id) {
    setItems((current) => current.filter((item) => item.id !== id));
  }

  return (
    <>
      <ul>
        {items.map((item) => (
          <li key={item.id}>
            {item.name}
            <input
              type="number"
              min="0"
              value={item.quantity}
              onChange={(event) => setQuantity(item.id, Number(event.target.value))}
            />
            <button type="button" onClick={() => remove(item.id)}>
              Remove
            </button>
          </li>
        ))}
      </ul>
      <p aria-live="polite">Total: {total.toFixed(2)}</p>
    </>
  );
}

Language: JSX
// The version that needs fixing: a stored total chasing the list.
const [items, setItems] = useState(initialItems);
const [total, setTotal] = useState(0);

useEffect(() => {
  setTotal(items.reduce((sum, item) => sum + item.price * item.quantity, 0));
}, [items]);
// First render shows 0, then a second render shows the real total.

Explanation

total is a pure function of items, so React already has everything needed to compute it while rendering. Deriving it means any state update to items — from any handler, now or added later — produces the correct total in the same render, because there is nothing to keep in sync. The stored version cannot promise that: the effect runs after the browser has painted, so the first pass shows the stale value and a second render follows it, which is where the flicker of a total correcting itself comes from.

The effect variant is worse than merely late. Because the effect depends on items and calls setTotal, the component renders twice per change, and any handler that reads total in the same tick as a mutation — for example to disable a checkout button — sees the pre-update value. Derived values computed in render are always current for the render they belong to.

useMemo is a separate question. Wrapping the reduce in a memo does not change correctness, only the cost of recomputation, and for a cart of a dozen items the arithmetic is not measurable. Add the memo when a real profile shows the pass is expensive — a long list, a sort plus a group, or a parse — and keep the derived expression otherwise.

Assumptions

  • React 19 function component with hooks; the fragment and aria-live paragraph are present so the announced total is accessible.
  • Prices are numbers in the smallest unit you intend to display. For currency arithmetic, multiply by a scale factor or use integer cents, because float addition drifts.

Common mistakes

  • Copying a prop or a computed value into state so it can be “updated later”, then discovering the copy survives a data change.
  • Recomputing inside setState callbacks so the total is right but no longer a single source of truth.

Caveats

  • A derived value recomputes on every render; if the input list is large, memoise the derivation itself rather than storing its result in state.
  • If the total must be editable by the reader, it is no longer derived — it is genuine state, and the two should be named differently.

Alternatives

Storing the total makes reads free and every write a place to forget; deriving makes reads cheap arithmetic and removes the failure mode entirely. A memo boundary adds nothing until a measurement shows the arithmetic is the bottleneck.

Follow-up

Examples that treat this as a prerequisite

Derived at build time from the editorial graph; no edge is invented here.

Related examples

Editorial links first, then deterministic same-task or same-topic candidates. Tags and shared language alone never qualify a candidate.

References