open atlas
↑ Back to track
React patterns, senior RXP · 05 · 02

Derive, don't store

Anything you can compute from existing props or state is derived, not state — calculate it during render instead of mirroring it into another useState with an effect, which creates two sources of truth that drift out of sync.

RXP Middle ◷ 18 min
Level
FoundationsJuniorMiddleSenior

You have firstName and lastName in state, and you need fullName. So you add a third useState and an effect that writes firstName + " " + lastName into it whenever either changes. It works — until a code path sets firstName without the effect having run yet, and for one render the screen shows a name that’s half old, half new. You’ve created a bug that only exists because the value was stored.

The fix isn’t a better effect. It’s deleting the state entirely. fullName was never state — it was a function of state, and the place to compute a function of state is during render. This lesson is about the single most common useEffect anti-pattern, and the one-line discipline that removes it.

Goal

After this lesson you can tell derived values apart from state, and reach for the right one by reflex; replace a useState + useEffect “mirror” with a plain const computed during render; compute filtered/sorted/grouped data in render instead of storing it; explain why stored-derived state is a two-sources-of-truth bug waiting to drift; and know the two ways this goes wrong — memoizing a trivial derivation by reflex (pure noise), and the rarer mistake of treating a genuinely expensive cached result as if it were over-derivation.

1

If you can compute it from existing props or state, it isn’t state — it’s derived. State is the minimal set of values React can’t recreate on its own: user input, server responses, things with their own lifecycle. Everything else is a function of that minimal set. The test is one question: given the props and the other state, can I always recompute this? If yes, it does not belong in useState.

// firstName and lastName are real state (user types them).
// fullName is NOT — it's always recomputable from the two.
const [firstName, setFirstName] = useState("Ada");
const [lastName, setLastName] = useState("Lovelace");
const fullName = `${firstName} ${lastName}`; // derived, in render

fullName updates the instant either input changes, because it’s recomputed every render. There is no second value to keep in sync, because there is no second value at all.

2

Mirroring one state into another with an effect creates two sources of truth that drift. The moment fullName lives in its own useState, the truth (“what the name should be”) exists in two places — and an effect is your fragile promise to keep them equal. That promise breaks on the most ordinary paths: the effect runs after paint, so the first render shows stale data (a flash); a reset that sets firstName but is then overridden leaves the mirror stale; and you’ve added an extra render every change, because the effect’s setFullName schedules a second pass.

// ANTI-PATTERN: a second source of truth, kept in sync by hand
const [fullName, setFullName] = useState("");
useEffect(() => {
  setFullName(`${firstName} ${lastName}`); // extra render + can lag reality
}, [firstName, lastName]);

Every bug here is a synchronization bug, and synchronization bugs vanish when there’s only one thing to be true. Deleting the state deletes the entire class of failure.

3

Filtered, sorted, and grouped lists are derived too — compute them in render, don’t store them. The same reflex that adds a fullName state adds a visibleTodos state with an effect that re-filters whenever todos or filter changes. It has the identical flaw: the filtered list can lag the real list for a render, and you pay an extra render every time either input moves. The filtered list is f(todos, filter) — so write it as that.

// derive at render time — always consistent with its inputs
const visibleTodos = todos
  .filter((t) => filter === "all" || t.status === filter)
  .sort((a, b) => a.createdAt - b.createdAt);

No effect, no extra state, no extra render, and visibleTodos is never out of step with todos or filter — because it’s literally derived from them on the spot.

4

Only wrap a derivation in useMemo when it’s actually expensive — and even then, memoize the work, not the truth. The previous steps remove an effect; they don’t justify reflexively wrapping every derived const in useMemo. fullName and a filter over a few hundred items are cheap — memoizing them is noise that adds a dependency array to maintain and buys nothing. useMemo is a performance escape hatch for a derivation that is genuinely slow (sorting tens of thousands of rows, a heavy transform) or that must keep a stable reference for a memoized child or an effect dependency.

// justified: the input is large and the work is real
const sorted = useMemo(
  () => bigList.slice().sort(expensiveComparator),
  [bigList],
);

Crucially, useMemo is still deriving in render — it’s the same pattern, just cached. It is not a return to storing state. The value still has one source of truth; you’ve only memoized its computation.

Worked example

A profile form: kill the mirror, derive the list. The original stores two values it could compute, and pays for both with effects.

function Profile({ todos }: { todos: Todo[] }) {
  const [firstName, setFirstName] = useState("Ada");
  const [lastName, setLastName] = useState("Lovelace");
  const [filter, setFilter] = useState<Filter>("all");

  // BUG #1: fullName mirrored — flashes stale, extra render
  const [fullName, setFullName] = useState("");
  useEffect(() => {
    setFullName(`${firstName} ${lastName}`);
  }, [firstName, lastName]);

  // BUG #2: visibleTodos mirrored — can lag todos/filter by a render
  const [visibleTodos, setVisibleTodos] = useState<Todo[]>([]);
  useEffect(() => {
    setVisibleTodos(todos.filter((t) => filter === "all" || t.status === filter));
  }, [todos, filter]);

  return <Panel name={fullName} items={visibleTodos} /* … */ />;
}

Both effects are the same anti-pattern: a useState that holds something already determined by other state, plus an effect to keep it equal. Delete both. The values become plain consts computed during render:

function Profile({ todos }: { todos: Todo[] }) {
  const [firstName, setFirstName] = useState("Ada");
  const [lastName, setLastName] = useState("Lovelace");
  const [filter, setFilter] = useState<Filter>("all");

  // derived: one source of truth, never stale, no extra render
  const fullName = `${firstName} ${lastName}`;
  const visibleTodos = todos.filter((t) => filter === "all" || t.status === filter);

  return <Panel name={fullName} items={visibleTodos} /* … */ />;
}

Two useStates gone, two effects gone, two render passes saved per change, and two whole categories of drift bug deleted. visibleTodos here is a small filter, so it stays a plain const — wrapping it in useMemo would be the reflex-memoization noise from Step 4. If todos were tens of thousands of rows with a heavy sort, then useMemo(() => …, [todos, filter]) earns its place — still derived in render, just cached.

Why this works

Why is “derive, don’t store” the highest-leverage rule in effect hygiene? Because effects that synchronize React state with other React state are the single most common misuse of useEffect, and almost all of them collapse into a derivation. Effects exist to synchronize with systems outside React — the DOM, a subscription, the network. State-to-state mirroring is inside React, where you don’t need an effect at all; you just compute. Removing it isn’t only fewer lines: it removes a render, removes a stale-frame flash, and removes a dependency array you’d otherwise have to keep correct forever.

Common mistake

The reflex mistake runs the other way once people learn this rule: wrapping every derived const in useMemo “to be safe”. A string concat and a filter over 200 items recompute in microseconds; useMemo there adds a dependency array, a closure, and reading overhead for zero measurable gain — it’s cargo-culting performance. Memoize when you’ve identified real cost (large data, an expensive transform) or when you need referential stability for a memo’d child or an effect dep. The rarer inverse mistake is the false alarm: seeing a useMemo and “simplifying” it to a plain const, when it was actually guarding a genuinely expensive computation or a stable reference. Read why the memo exists before removing it.

Check yourself
Quiz

A component holds selectedId in state and, to show the selected row, keeps a second selectedRow state synced by an effect: useEffect(() => setSelectedRow(rows.find(r => r.id === selectedId)), [rows, selectedId]). What's the senior fix?

Recap

A value you can recompute from existing props or state is derived, not state — so compute it during render as a plain const, never mirror it into a second useState kept in sync by a useEffect. Mirroring forks the truth into two places, and the effect is a fragile hand-written promise to keep them equal: it flashes stale on first render, can lag reality on ordinary code paths, and costs an extra render every change. Deleting the state deletes the whole class of synchronization bug, because there’s only one thing left to be true. This applies just as much to filtered, sorted, and grouped listsf(todos, filter) belongs in render, not in visibleTodos state. Reach for useMemo only when the derivation is genuinely expensive or needs a stable reference; it still derives in render, just cached — it is not a license to store derived state again. And resist the two reflexes: memoizing trivial derivations (noise) and, rarely, “simplifying” away a memo that was guarding real cost. The discipline is one line: if you can derive it, don’t store it.

Practice

Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.

recallapplystretch0 of 4 done

Something unclear?

Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.

shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.