Skip to content
Chapter 24Lesson 2

Derive in render, do not mirror into state

A React state-design rule for keeping derived values out of useState, so your component has one source of truth instead of two that drift apart.

A profile renders a person’s name from two pieces of state, firstName and lastName, and you need a fullName for the heading.

The reflex almost every React beginner reaches for is to give fullName its own useState, then wire up a useEffect that recomputes it whenever either input changes:

const [firstName, setFirstName] = useState('Ada');
const [lastName, setLastName] = useState('Lovelace');
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);

It works: the heading shows the right name. But it is wasteful, briefly stale, and bug-prone, and the next section traces exactly how.

The previous lesson asked one question before reaching for useState: what kind of value is this? When the answer is “computed from other state or props,” the instruction was to derive it in render rather than store it. Here that fix is a single line:

const fullName = firstName + ' ' + lastName;

Delete the state and the effect, and compute the value where you use it. The rest of this lesson is why that line is correct, why it is cheaper, and how to spot the cases that genuinely do need state.

The double-render tax of syncing state with an effect

Section titled “The double-render tax of syncing state with an effect”

Here is the mirror-and-sync version in full, written the way a beginner would write it.

const Profile = () => {
const [firstName, setFirstName] = useState('Ada');
const [lastName, setLastName] = useState('Lovelace');
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
return <h1>{fullName}</h1>;
};

Now trace what happens when the user types a letter into the first-name field. In the render model chapter you learned that an update flows through four phases: trigger, render, reconcile, commit. Here that whole sequence runs once, and then runs again.

pass 1 Render #1
then Commit
then Effect
then Render #2
then Commit
screen — what the user sees
input Adam
h1 Ada Lovelace stale

The input already says Adam, but the heading still reads the old fullName.

firstName changes to 'Adam'. Render #1 runs — but fullName still holds 'Ada Lovelace' from the last commit, so the heading is built from stale state.
done Render #1
now Commit
then Effect
then Render #2
then Commit
screen — what the user sees
input Adam
h1 Ada Lovelace stale

This wrong frame is real — it is painted to the screen, not just held in memory.

Commit. React paints that render to the DOM. For this frame, the screen genuinely shows the old name.
done Render #1
done Commit
now Effect
→ schedules re-render
screen — what the user sees
input Adam
h1 Ada Lovelace stale

setFullName runs after the commit — too late for this frame. It only queues a second pass.

Now the effect runs and calls setFullName('Adam Lovelace'). That schedules another render — the screen has not changed yet.
done Render #1
done Commit
done Effect
now Render #2
now Commit
screen — what the user sees
input Adam
h1 Adam Lovelace correct

One keystroke, two renders, and a frame of stale UI in between — all to produce one value.

Render #2 and Commit. Only now — on the second render — does the heading show the right name.
one pass Render
then Commit
const fullName = firstName + ' ' + lastName
screen — what the user sees
input Adam
h1 Adam Lovelace correct

The heading is computed in the render body, so it is right on the first paint. No second render, nothing to keep in sync.

Derive it instead, and there is no second render and no stale frame. One render computes the name and paints it correctly the first time.

One keystroke produced two renders, and in the gap between them the heading showed a name that was already out of date. You will not see the flicker on a fast machine, but it is real, and a slower render or a heavier value makes it visible.

The second copy is the deeper problem. firstName and lastName are the truth; fullName is a copy you have promised to keep current by hand, and that promise lives in the dependency array. The day a teammate adds a middleName and updates the template but not the array, fullName stops tracking it: no crash, no warning, just a subtly wrong heading that someone catches in production. The derived version cannot have this bug, because there is no second copy to fall out of sync.

That second copy of a value that already exists, recomputed by an effect, is what we will call derived state . The problem is not the derived value, it is storing it. The cure is to compute it where the render already has everything it needs.

The shift in mindset is JavaScript before JSX. A component body is an ordinary function that runs top to bottom on every render, and everything above the return is just code. A variable, a ternary, a .filter, a .reduce, a string template: anything you can express in plain JavaScript gets computed right there, from the values the render already holds.

It already holds them. As you saw in the render-model chapter, props and state are frozen constants for one render, a snapshot. So firstName and lastName are two constants in scope, and firstName + ' ' + lastName is just reading them and concatenating. The JSX at the bottom reads the local variable you computed. No hook, no second render.

This is why state is the minimum set of values from which everything else can be computed. Every piece of state is a value that changes over time, that you can get wrong, and that can disagree with another value, so you keep it as small as possible. Putting a derivable value in useState means maintaining two sources of truth that can drift apart; deriving keeps one source of truth plus a view computed fresh from it every time.

Once you start looking, derivable values are everywhere, each one a single expression in the body:

const fullName = firstName + ' ' + lastName;
const completedCount = todos.filter((todo) => todo.isDone).length;
const cartTotal = items.reduce((sum, item) => sum + item.price, 0);
const isEmpty = items.length === 0;
const selectedItem = items.find((item) => item.id === selectedId);

None of those belong in state; each is computed from state that already exists.

Here is the same fullName component both ways, side by side.

const Profile = () => {
const [firstName, setFirstName] = useState('Ada');
const [lastName, setLastName] = useState('Lovelace');
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
return <h1>{fullName}</h1>;
};

Two renders and a stale frame. fullName is a second copy of data that already lives in firstName and lastName, kept in sync by hand. Every name change costs an extra render, and the day a new name part is added but the dependency array is not, the heading silently goes stale.

The clearest way to internalize this is to do the deletion yourself. The next exercise hands you the mirror-and-sync version to strip down.

This profile keeps fullName in a third piece of state and syncs it with an effect. Delete the fullName state and the effect, and compute fullName during render instead. Type in the inputs and confirm the heading still tracks them — with less code.

Preview
Reference solution
import { useState } from 'react';
export const App = () => {
const [firstName, setFirstName] = useState('Ada');
const [lastName, setLastName] = useState('Lovelace');
const fullName = firstName + ' ' + lastName;
return (
<div className="space-y-3 p-4">
<input
className="block rounded border px-2 py-1"
value={firstName}
onChange={(event) => setFirstName(event.target.value)}
/>
<input
className="block rounded border px-2 py-1"
value={lastName}
onChange={(event) => setLastName(event.target.value)}
/>
<h1 className="text-xl font-semibold">{fullName}</h1>
</div>
);
};

The fullName state and the useEffect are gone, replaced by one const fullName = firstName + ' ' + lastName; in the body. The useEffect import goes with them, leaving only useState.

You removed four things to read and maintain: a useState, a useEffect, an import, and a dependency array. In their place is one line that concatenates two strings, shorter and impossible to get out of sync.

The fullName case is the textbook example, but the shape it belongs to is far broader. Once you can name that shape, you catch it everywhere. In the abstract:

a useState holding a value, plus a useEffect watching other state or props and calling that state’s setter.

Wherever those two things appear together, you are almost always looking at the same mistake, and the cure is always the same: delete both, compute the value in render.

const [derived, setDerived] = useState(initialValue);
useEffect(() => {
setDerived(computeFrom(source));
}, [source]);

State whose only job is to hold a value computed from other state. It is a copy, not something the user or the outside world sets directly.

const [derived, setDerived] = useState(initialValue);
useEffect(() => {
setDerived(computeFrom(source));
}, [source]);

A setter called from an effect watching the source: the hand-maintained promise to keep the copy current. It costs an extra render and goes stale when the dependency list falls behind. When a setter inside an effect exists only to mirror other state, delete both and compute the value in the body.

1 / 1

A todo app filters its list: show all, show active, or show completed. The list you display is the source list run through a filter. The reflex, again, is to store the filtered result and resync it whenever the inputs change:

const [visibleTodos, setVisibleTodos] = useState([]);
useEffect(() => {
setVisibleTodos(getFilteredTodos(todos, filter));
}, [todos, filter]);

Same shape, same fix. visibleTodos is fully determined by todos and filter, so compute it in the body:

const visibleTodos = getFilteredTodos(todos, filter);

You may object: filtering is real work, so isn’t running it on every render wasteful? The short answer is that it is cheap, and for the rare case where it is not, the React Compiler handles it (next section).

Flags are the most obviously derivable values there are, and where this mistake hides most often. A form wants to know whether it has any errors, so it can disable the submit button or show a banner. The errors live in state, and the flag is computed from them:

const [hasErrors, setHasErrors] = useState(false);
useEffect(() => {
setHasErrors(errors.length > 0);
}, [errors]);

That is three lines, an effect, and a dependency array to express a single comparison:

const hasErrors = errors.length > 0;

The same goes for every flag of this kind: isEmpty, isComplete, canSubmit. If a boolean is “true when some condition over my state holds,” it is one expression in the body, never a piece of state with an effect keeping it current.

You have now seen the shape three times; the skill is recognizing it on the fourth. For each value below, decide whether it genuinely needs useState or whether it should be derived in render.

Sort each value into whether it should live in `useState` or be computed during render. Drag each item into the bucket it belongs to, then press Check.

Lives in useState Changes independently; the UI reads it
Derive in render Computable from other state or props
The text the user has typed into a search box
Whether a modal is open
The list of todos fetched from the server
The id of the currently selected todo
The user’s full name, from firstName and lastName
The number of completed todos
Whether the form has any errors
The todos that match the current filter

This is the objection that keeps people caching in state, and the answer runs opposite to most beginners’ intuition.

For the work that shows up in real components, a .filter over a few hundred rows, a sum across a cart, a string concatenation, a .find, the cost is negligible next to the render around it. Building the React elements and reconciling them against the previous tree is the expensive part; your .filter is a rounding error beside it. Recomputing every render is not a problem to solve, it is how the value stays correct with zero bookkeeping on your part.

React’s own rule of thumb: unless you are creating or looping over many thousands of objects, a computation is probably not expensive. Below that, derive freely.

When you are unsure, measure rather than guess. Wrap the computation in a timer and read the number off a representative dataset:

console.time('filter');
const visibleTodos = getFilteredTodos(todos, filter);
console.timeEnd('filter');

If that consistently prints a millisecond or more on real data, then you have a candidate worth optimizing. If it prints 0.02ms, do nothing. A hunch that filtering must be slow is not a reason to add a second source of truth.

You also get a backstop for free. This project ships with the React Compiler turned on: it reads your components at build time and caches the derivations whose inputs have not changed, reaching cases the old manual tools could not. So derive in render, write natural code, and let the compiler handle the caching. You describe the value; the build step optimizes it.

For the rare derivation that is measurably expensive and you need to cache yourself, React provides useMemo. Notice the comment, because it is the whole story:

const sortedRows = useMemo(
() => expensiveSort(rows),
[rows],
); // measured at ~12ms over 40k rows

The default is no useMemo. You reach for it only after a measurement crosses the threshold, and when you do, you leave behind the number that justified it. The full decision framework comes in a later chapter; for now, recognize the shape and leave it out by default. Memoization is mostly the compiler’s job.

The danger with a rule this sharp is over-correcting into “derive everything,” which is just as wrong. Plenty of values do belong in useState, and four triggers mark them. If a value matches one, it is state; if it matches none, look harder, because it is probably derivable.

  1. It originates from user input. The raw text in an <input>, a toggle’s on or off, which tab is selected. There is nothing to compute these from, because the user is the source. This is the seed of every controlled input, covered in the forms unit later.

    const [query, setQuery] = useState('');
  2. It is cached from an external system. Data fetched from the server, read from localStorage, or pushed by a subscription needs a local home so the UI can read it synchronously as it renders.

    const [todos, setTodos] = useState<Todo[]>([]);

    Holding server state in useState is a stopgap, not its real home; a server-state cache or Server Component, covered in a later unit, is. It still counts as a legitimate trigger, with that caveat.

  3. It captures a moment in time. A timestamp taken once at mount, or a random seed assigned a single time: values you deliberately snapshot and must not recompute. Calling Date.now() or Math.random() in the render body is impure because it gives a different answer each render, so you capture the value once and hold it in state.

    const [startedAt] = useState(() => Date.now());
  4. It is an intentionally divergent draft. An editable copy of a server value that is supposed to drift from the canonical record until the user saves. This is the legitimate version of seeding state from a prop, where the next section picks back up.

One question sorts all of this:

Updating state when a prop changes: derive, key, or lift

Section titled “Updating state when a prop changes: derive, key, or lift”

One situation feels more than any other like it demands a sync effect: I have local state seeded from a prop, and when the prop changes I need my state to match.

Picture a list with a selectedId prop and a panel showing the full selected item. That thought produces a second piece of state holding the item, kept in sync with the prop by an effect:

const [selectedItem, setSelectedItem] = useState(null);
useEffect(() => {
setSelectedItem(items.find((item) => item.id === selectedId) ?? null);
}, [items, selectedId]);

It is almost always one of three things, none of them a sync effect. Naming which one you are in tells you the right tool.

const [selectedItem, setSelectedItem] = useState(null);
useEffect(() => {
setSelectedItem(items.find((item) => item.id === selectedId) ?? null);
}, [items, selectedId]);

The mistake. An effect whose only job is to copy something computed from a prop into local state. Two renders, a stale frame, and a second source of truth to maintain.

The three cases:

  1. The value is purely a function of the prop, so derive it. No state: compute it in render, as in the first half of this lesson. Given a selectedId and a list, the selected item is items.find((item) => item.id === selectedId).

  2. The state is an editable copy that should reset when the prop’s identity changes, so use a key-reset. From the render-model chapter: <EditForm key={record.id} record={record} /> remounts the form on a new record, so its useState(record.field) re-seeds for free and the old state is simply discarded.

  3. Two or more components need the value, so lift it to their common parent. The child stops owning a copy and reads the parent’s single source of truth. That is the next lesson; it is named here only so you recognize the case.

So the rule, stated plainly: “I need to update state when a prop changes” is a warning sign, and the cure is to derive, key-reset, or lift, never to add a mirroring effect. This is the fix the previous lesson promised for the frozen-prop trap.

There is one documented exception, named once so you recognize it, not so you reach for it. React permits calling a setter during render — not in an effect — to adjust state when a prop has changed, guarded by a comparison against the previous value:

const [prevId, setPrevId] = useState(selectedId);
if (selectedId !== prevId) {
setPrevId(selectedId);
setSelection(null);
}

Because the setter runs during render rather than after commit, React re-renders immediately, before painting the children, so the user sees no stale frame and there is no second commit. But it is hard to read, and the guard is easy to get wrong: drop the selectedId !== prevId check and it loops forever. Reach for key or render-time computation first; this is the last resort, for adjusting some state when a prop changes.

Now check that the routing reflex stuck: read the scenario and pick the senior fix.

An <InvoiceEditor> receives a customer prop and, on mount, copies the customer’s billing address into local useState so a biller can tweak it before sending the invoice — the draft is meant to diverge from the saved record until they hit Save. The bug: when the biller switches to a different customer in the sidebar, the editor still shows the previous customer’s address and half-typed edits. You want the editor to start clean for each customer while keeping the draft editable. What is the cleanest fix?

Watch customer.id from an effect and call the setters to overwrite the draft fields whenever it changes.
Pass the customer’s id as the editor’s key, so switching customers throws away the old instance and mounts a fresh one seeded from the new address.
Stop storing the draft in state and read each field straight off the customer prop in the render body.
Move the draft state up into the sidebar’s parent and pass it back down so it survives the switch.

Choosing where a value lives: the derive-first filter

Section titled “Choosing where a value lives: the derive-first filter”

The whole lesson reduces to four questions, asked in order, before a value is allowed into useState.

%%{init: {'themeCSS': '.node.leaf .nodeLabel { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; }'} }%%
flowchart LR
  start([A value your<br/>component needs])
  q1{"Computable from<br/>props or other<br/>state right now?"}
  q2{"A draft that resets<br/>on an identity<br/>change?"}
  q3{"Needed by 2+<br/>components?"}

  derive["<b>Derive in render</b><br/>this lesson"]
  keyreset["<b>key-reset</b><br/>render-model chapter"]
  lift["<b>Lift to parent</b><br/>next lesson"]
  usestate["<b>useState ✓</b><br/>previous lesson"]

  start --> q1
  q1 -- Yes --> derive
  q1 -- No --> q2
  q2 -- Yes --> keyreset
  q2 -- No --> q3
  q3 -- Yes --> lift
  q3 -- No --> usestate

  class derive,keyreset,lift offramp
  class usestate leaf
  classDef offramp fill:#1f2937,stroke:#94a3b8,color:#f8fafc
  classDef leaf fill:#bbf7d0,stroke:#15803d,color:#111,stroke-width:2px
The four questions in order; useState is what you reach only after the first three say no.
  1. Can I compute it from props or other state? Then derive it in render. (This lesson.)
  2. Is it a draft that must reset when an identity changes? Then key-reset. (The render-model chapter.)
  3. Do two or more components need it? Then lift it to their common parent. (The next lesson.)
  4. Otherwise, is it raw input, cached external data, a captured moment, or a divergent draft the JSX reads? Then it earns a useState. (The previous lesson.)

So reach for useState last, not first. State is the small, hard core left once you have ruled the other three out, the minimum set of values from which everything else is computed.

The single best follow-up to this lesson is the React documentation page it is built on. The examples you saw here, the full name, the filtered list, the error flag, and the prop reset, are drawn almost directly from it, and the pages below carry several more, plus a lint rule that catches the same mistake in your own code.