StateGENERALFRAMEWORK-SPECIFICDEVICE-SPECIFIC

Derived State

If a value can be computed reliably from state you already hold, storing it separately creates a second owner — and duplicated state is state that can disagree with itself.

The intent, the obvious build, and why it breaks

Every lesson starts where the work starts: someone wanted an outcome, and the first implementation that comes to mind has a problem.

The question

Should I store this value, or compute it from what I already have?

The user intent

A person types a first name and a last name and expects the greeting, the header, the avatar initials and the confirmation email to all say the same thing as what they just typed.

The obvious build

Keep fullName in state next to firstName and lastName, and update it whenever either changes. Computing it every render seems wasteful, and having it ready is simpler for the components that need it.

Why it breaks

One code path updates firstName without updating fullName — a reset, a bulk import, an undo, a form prefill — and the header now says a name the fields do not.

How it breaks in a real browser
  • One code path updates firstName without updating fullName — a reset, a bulk import, an undo, a form prefill — and the header now says a name the fields do not.
  • The mismatch is not a crash. It is a subtly wrong value that renders happily, which is why it reaches production and stays there.
  • Every new writer of firstName inherits an obligation to also write fullName, and nothing in the type system says so. The invariant lives in code review and leaves with the reviewer.
  • Adding a third derivation — initials, sort key, slug — multiplies the obligation rather than adding to it, because each writer now has three things to remember.
  • A "sync" effect that recomputes the copy after the fact renders once with the stale value, then again with the correct one, which is a visible flash and an extra pass through style and layout (The Cost of a Change).
  • When the two disagree, there is no way to tell which is right — both are state, both were written by code, and neither is marked as the source (Debugging State).
IntentEventStateUI LogicDOM WorkNetworkLayout / PaintPixelsFeedback

What is actually happening

In the browser, not in the framework.

  • Derived state is not a technique; it is the absence of one. A value with exactly one owner and a pure function to compute everything else has no synchronisation problem, because there is nothing to synchronise.
  • Storing a computed value creates a second owner for the same fact. From then on, correctness depends on every writer of the source also writing the copy — an invariant maintained by discipline rather than by structure (State Synchronization).
  • Recomputation on render is usually cheap: string concatenation, a comparison, a filter over an array, a sum. The framework was going to walk that render anyway; the derivation rides along.
  • Recomputation is *not* always cheap: sorting tens of thousands of rows, running a fuzzy search, building an index, parsing large text, or anything that allocates proportionally to a big input. Those are the cases where caching the result — memoization — earns its cost (Memoization).
  • Memoization is itself a cache, so it has a cache's problems: a key, an invalidation rule, retained memory, and a comparison cost paid on every render whether or not the value changed.
  • Frameworks differ sharply here. In signal-based systems a derivation is a node in the reactive graph that recomputes lazily when a dependency actually changes; in re-render systems it is a function call that runs every time the component runs unless you wrap it (Reactivity Models).
  • The important asymmetry: a stale derivation is impossible when you derive, and routine when you copy. Recomputation costs CPU you can measure; duplication costs correctness you cannot.

What this makes the browser do

And which of it is avoidable.

  • A derivation on render is main-thread work inside the render pass. For most derivations it is a rounding error next to the DOM reconciliation and the style recalculation that follow (What a Component Costs to Render).
  • A memoized derivation replaces that work with a dependency comparison plus retained memory. If the comparison touches a large object or the dependencies change every render, you have added work rather than removed it.
  • An effect-based "sync" — write the source, then recompute the copy in an effect — costs an extra render and, if it runs after paint, a visible intermediate frame (The Frontend Reasoning Loop).
  • Deriving from an unstable identity — a new array or object literal per render — defeats every memo downstream of it, which is how a page ends up doing all the work plus all the caching.
  • Genuinely heavy derivations do not belong on the main thread at all; a worker keeps the input responsive while the result is computed (When a Worker Is Actually the Answer).

One owner, or two things that can disagree

The argument for deriving is not elegance. It is that a stored copy has no way to be right — it is only ever as correct as the last writer who remembered it, and that obligation is invisible everywhere the source is written.

Notice what the second version deletes: not just a line of state, but the entire class of bug where the header and the fields disagree. There is no code path that can produce a stale fullName, because there is no fullName to be stale.

A name in two places, and in one
Stored
state = { firstName: '', lastName: '', fullName: '' }

setFirstName(v) {
  state.firstName = v
  state.fullName = v + ' ' + state.lastName   // every writer must remember
}

// and then, six months later, in an unrelated file:
resetForm() { state.firstName = ''; state.lastName = '' }  // fullName is now wrong
Derived
state = { firstName: '', lastName: '' }

const fullName = () => `${state.firstName} ${state.lastName}`.trim()
const initials = () => [state.firstName, state.lastName]
  .filter(Boolean).map((n) => n[0].toUpperCase()).join('')

// resetForm() cannot break either of them: there is nothing to update.

The first version distributes an invariant across every present and future writer of two fields, enforced by nothing. The second makes the invariant structural — the derived values cannot disagree with the source because they are the source, read through a function. Adding initials costs one line and imposes no new obligation on anyone.

When caching a derivation actually pays

Deriving is the default, not a rule. There are derivations where recomputation is genuinely expensive, and pretending otherwise produces a text field that drops keystrokes. The honest position is that this is a measured decision with four possible answers, and "add a memo" is only one of them.

Two things make the decision harder than it looks. First, the comparison a memo performs is not free, so memoizing a cheap derivation is a net loss. Second, memoizing a derivation of an unstable input does nothing at all, because the dependency changes every render — the fix there is upstream, at the identity of the input.

This derivation is showing up in a profile. Now what?

What is the cheapest correct way to stop paying for this derivation?

Do nothing

when The derivation is a concatenation, a comparison, a boolean, a sum, or a pass over a list short enough that the render around it dominates. This is the large majority of derivations.

cost Nothing, and it stays correct by construction. The only cost is the temptation to optimise it anyway.

Stabilise the input instead

when The derivation is cheap but its input is a new array or object every render, causing re-renders or defeating memos downstream.

cost A small refactor at the source — hoisting a literal, keying a list properly — which is usually the real fix that a memo was papering over (Reconciliation and Keys).

Memoize the derivation

when A profile shows the computation is expensive relative to the render, its inputs are stable, and they change less often than the component re-renders: a sort or a group-by over thousands of rows.

cost A dependency comparison on every render, retained memory for the cached result and its closure, and one more place a wrong dependency list can hide a stale value (Memoization).

Move it off the main thread

when The computation is heavy enough to be a long task regardless of caching — a large parse, an index build, a fuzzy search over a big corpus — and blocks input while it runs.

cost A worker boundary, structured-clone or transfer costs on the input and output, asynchrony in a place that used to be synchronous, and a loading state to design (When a Worker Is Actually the Answer).

Do less work

when The derivation runs over far more data than the user can see: sorting 40,000 rows to display 20.

cost Moving the work to the server or paginating, which changes the contract and adds a network round trip but removes the computation entirely (Pagination From the Interface Backwards).

How duplicated state actually shows up

The failures below have one thing in common: none of them throws. A duplicated value that has gone stale renders as confidently as a correct one, which is why these are found by users rather than by tests.

Duplication, memoization and their symptoms
TriggerSymptomCauseResponse
A new writer updates the source but not the copyThe header shows a different name from the input fieldsAn invariant maintained by convention across every call siteDelete the copy and compute it at the point of use.
A copy is kept in sync by an effectA brief flash of the previous value on every changeThe effect runs after the render that used the stale copyDerive during render instead of synchronising after it.
A memo has an incomplete dependency listA value that updates for some changes and not othersThe cache is never invalidated for the missing dependencyLet the linter own dependency lists, and prefer deriving where the memo was not measured to be needed.
Everything is memoized as a policyNo hot spot in the profile, but the whole app is slowerComparison cost and retained memory paid on every render everywhereRemove memos that were not justified by a measurement (Measure Before Optimising).
A memo depends on an object literal built during renderThe memo never hitsThe dependency identity changes every renderFix the identity upstream; the memo was treating a symptom.
A derivation reads the clock or a random valueServer-rendered HTML is replaced on hydrationThe derivation is not a pure function of stateMove the impure input into state, set after mount (Hydration Mismatch).
A filtered list is stored rather than derivedA row edited in place disappears from the filtered view but not the raw oneTwo representations of one collectionStore the rows and the filter; derive the view.

How to build it

Most important first.

  • Default to deriving. Keep the minimal set of values from which everything visible can be computed, and compute the rest at the point of use.
  • Identify the minimum first: for a filtered, sorted, paginated table, the state is the raw rows, the filter, the sort and the page. The visible rows are a derivation, and storing them is the bug (List Virtualization).
  • Only memoize when a measurement says the derivation is expensive relative to the render, or when a stable identity is needed to keep something downstream from re-rendering. Both are real reasons; neither is a default (Memoization).
  • Prefer a stable source over a memoized derivation of an unstable one. Fixing the identity at the source removes the need for the memo entirely.
  • When two values must genuinely both exist — a draft and a saved record, a client id and a server id — say so explicitly and write down which one wins and when (Form State Is a Draft).
  • Derive from the URL rather than mirroring it, for the same reason: one owner, many readers (The URL Is Application State).
  • If the derivation is expensive and the input is large, move it off the main thread before reaching for a cache (Web Workers and the DOM Boundary).

Keyboard, focus, semantics, announcement

A required field on every lesson in this domain, not a section added when there is room.

  • A disagreement between duplicated values is often visible to a screen reader before it is visible on screen: the accessible name of a control comes from one derivation while the visible label comes from another, so what is announced differs from what is shown (The Accessibility Tree).
  • Derived counts and summaries are what make a change perceivable to a non-visual user. "12 of 4,000 orders" in a live region is a derivation, and it is the only announcement of a filter change that anyone gets (Live Regions and Announcement).
  • A derived aria-label or aria-describedby value must be computed from the same source as the visible text. Two sources produce a control whose announcement contradicts its appearance, which is worse than an unlabelled one (The Rules of ARIA).
  • An effect-based sync that renders once with the stale value causes the announcement to fire twice — first the old value, then the new — which a screen-reader user hears as a contradiction (Live Regions and Announcement).

What can go wrong

Failure modes
  • The classic: two owners, one fact, and a code path that updates only one. It renders fine and is wrong.
  • The mitigation failing: useMemo or its equivalent wrapped around a derivation whose dependency array is wrong. Now the value is stale *and* the staleness looks intentional, which is worse than not memoizing.
  • Memoizing everything on principle: every render pays comparison cost, memory grows with retained closures, and the profile is flatter but slower overall (The Real Cost of JavaScript).
  • Deriving from an async source without handling the in-between: the derivation runs on partial or absent data and produces a confidently wrong value (Loading, Error, Empty — The States You Did Not Render).
  • A derivation that is not actually pure — reading the clock, a random value, or a mutable module-level variable — producing different output for the same input and defeating both memoization and server rendering (Hydration Mismatch).
  • A derivation over a large list recomputed on every keystroke, blocking input while the user types (Long Tasks).
What can arrive out of order
  • A derivation over async data racing the arrival of that data: it runs against the previous response and renders a value that belongs to a request the user has moved on from (Out-of-Order Responses).
  • A memo whose dependency identity changes asynchronously — a new object from a fetch — recomputing at an unpredictable point relative to a user interaction.
  • An effect-based sync scheduled after paint, so a fast follow-up interaction reads the copy before the sync has run (The Microtask Checkpoint).
Security
  • Derived validity is a UI convenience and never an authorization decision. canDelete computed on the client is a rendering input; the server decides (What the Frontend Is Responsible For in Auth).
  • A derived value that filters out records the user should not see is not a security control — the unfiltered data is already in the client and readable in the devtools. Filter on the server (Over-Fetching and Under-Fetching).
  • Derivations that build markup — a highlighted search match, a formatted description — are HTML sinks. Derive text and let the framework escape it (Sanitization and Trusted HTML).
  • Memoization caches retain their inputs. A memo over personal data keeps that data alive after the component that needed it is gone (Memory Leaks).
Misreads
  • "Recomputing on every render is wasteful." Most derivations cost less than the comparison a memo would perform to avoid them. Measure the specific one; do not generalise (Memoization).
  • "Memoization makes things faster." It makes recomputation conditional and adds comparison and memory. Whether that is faster depends on the cost of the computation, the stability of the inputs, and how often they change (The Real Cost of JavaScript).
  • "Sync it with an effect." An effect that copies state into state is the duplication, plus an extra render, plus a window where the two disagree. Derive instead.
  • "Derived state means no state." It means the minimum state. Something must be authoritative; derivation only says the rest should not be.
  • "It only breaks if someone forgets." Correct — and someone always does, because the obligation is invisible at the call site of every writer.

Measuring it, and what changes in the field

How you would see this
  • Search for the copied value. If a name appears both as stored state and as something computed elsewhere, you have found the second owner (Debugging State).
  • The Performance panel shows whether a derivation is actually expensive. Record an interaction, find the render, and look at what the function cost — before adding a memo, not after (Measure Before Optimising).
  • Framework profilers attribute render time per component and show whether a memo prevented work or merely added a comparison (What a Component Costs to Render).
  • Interaction latency in the field tells you whether keystroke-time derivations are hurting real users on real devices (Interaction Responsiveness).
Slow device, slow network, large data, old tab
  • On a slow device, a derivation that is free on a laptop can be the difference between a responsive text field and a caret lagging behind the typing (Interaction Responsiveness).
  • With a large dataset, derivation cost scales with input size while duplication cost stays constant — which is exactly the case where memoizing or moving off-thread starts to pay.
  • With async data, the derivation must be defined for loading, empty, partial and error inputs, not just the happy one (Loading, Error, Empty — The States You Did Not Render).
  • On the server, during SSR, a derivation that reads the clock or the locale produces a different value than the client's and triggers a mismatch on hydration (Hydration Mismatch).
What this costs
  • Deriving trades CPU for correctness. That is a good trade until the CPU cost becomes user-visible, at which point you buy some of it back with a cache and take on the cache's invalidation problem.
  • Memoization is not free: a dependency comparison per render, retained memory, and one more place a stale value can hide behind an incorrect dependency list.
  • Deriving at the point of use can compute the same thing in several components. Usually irrelevant; occasionally the reason to hoist the derivation to a shared node in the reactive graph.
  • Refusing to duplicate can be dogmatic. A draft that intentionally differs from the record is duplication you want, and calling it derived state would be wrong (Form State Is a Draft).

Where this applies

Frontend advice ages badly and fragments across engines. These labels say what each claim is specific to, and where a different browser, device or framework would differ.

  • GENERALThe correctness argument — one owner, pure derivations — is independent of framework and of language. Only the cost side of the trade, and therefore when a cache pays, varies with the runtime.
  • FRAMEWORK-SPECIFICThis is where the five frameworks differ most. Vue's computed, Solid's createMemo, Svelte 5's $derived and Angular's computed() are cached nodes in a dependency graph: they recompute lazily and only when a dependency they actually read has changed, so deriving is the cheap default and explicit memoization is rarely needed. React re-runs the whole component function, so a derivation runs on every render unless wrapped in useMemo — and React documents useMemo as a hint the runtime may discard, which is why React-shaped advice about memoizing does not transfer to the signal frameworks and vice versa.
  • DEVICE-SPECIFICWhether a given derivation is "cheap" is a statement about a CPU. A sort that is invisible on a development laptop can be a blocking task on a mid-range phone, so the memoize-or-not decision must be made against field devices rather than the machine the code was written on.

Where the depth lives

This domain teaches the browser-side mechanism and hands the rest off.

Domains that do not exist yet
  • Software Design — a derived value is a function of state rather than a member of it, which is the same normalisation argument that removes redundant columns from a schema.