Ch. 3 · React

React Derived Values Without Extra State

Stop syncing state with useEffect: compute filtered lists, totals and validity during render in React, and use useMemo only for real cost.

~6 min readbeginnerupdated Oct 6, 2026

“You have a list of products and a search box. Where does the filtered list live?” A surprising number of candidates answer with a second useState for filteredProducts and a useEffect that updates it whenever the query changes. The interviewer is waiting for a simpler answer: it does not live anywhere. You calculate it.

This question tests whether you can design state, not just use hooks. Redundant state is one of the most common sources of React bugs: values that drift out of sync, extra renders, and effects that chain into each other until something loops.

Before you start

You should know useState, useEffect and the fact that a component function runs top to bottom on every render. Familiarity with array methods such as filter, reduce and find helps. Nothing here is version specific; React 19’s compiler can memoise calculations automatically, but the state-design principle is identical in React 18.

The short answer

Derived values are anything you can compute from existing props or state: a filtered list, a total, a full name, whether a form is valid. Do not store them in state and do not sync them with an effect. Compute them in the component body during render, so they are always consistent with their sources. If the calculation is genuinely expensive, wrap it in useMemo keyed on its inputs. Keep only the minimal source of truth in state.

How it works

Every render is a fresh call to your function. Any variable you declare in that call is recalculated from the current props and state, so it cannot be stale:

function ProductList({ products }) {
  const [query, setQuery] = useState('');

  const visible = products.filter(p =>
    p.name.toLowerCase().includes(query.toLowerCase())
  );

  return (
    <>
      <input value={query} onChange={e => setQuery(e.target.value)} />
      <p>{visible.length} of {products.length}</p>
      <ul>{visible.map(p => <li key={p.id}>{p.name}</li>)}</ul>
    </>
  );
}
JSX

Compare the effect-based version:

const [visible, setVisible] = useState(products);
useEffect(() => {
  setVisible(products.filter(p => p.name.includes(query)));
}, [products, query]);
JSX

When query changes, React renders with the old visible, commits that to the screen, runs the effect, sets state, and renders again. The user briefly sees a mismatch, you pay for two renders, and you now have two copies of the truth that must never disagree. If products comes from a parent that creates a new array every render, the dependency changes every time and the effect runs constantly.

Step-by-step walkthrough

Step 1: List every piece of state and ask “can I compute this?”

Take a checkout form with items, couponCode, subtotal, discount, total and isValid. Only items and couponCode come from the outside world or the user. The rest are functions of those two.

Step 2: Turn derived state into plain variables

function Checkout({ items }) {
  const [couponCode, setCouponCode] = useState('');

  const subtotal = items.reduce((sum, i) => sum + i.price * i.qty, 0);
  const discount = couponCode === 'SAVE10' ? subtotal * 0.1 : 0;
  const total = subtotal - discount;
  const isValid = items.length > 0 && total > 0;
  // ...
}
JSX

Four useState calls and their sync effects disappear. There is no order of operations to get wrong.

Step 3: Store identifiers, not copies

A classic redundant-state bug is storing the selected object:

const [selected, setSelected] = useState(null); // a copy of an item
JSX

If the item is edited in the list, selected keeps the old copy. Store the id and look it up:

const [selectedId, setSelectedId] = useState(null);
const selected = items.find(i => i.id === selectedId) ?? null;
JSX

Now an edit to the list is automatically reflected in the selection, and deleting the item makes selected null without extra code.

Step 4: Memoise only when you have measured a cost

If filtering ten thousand rows on every keystroke shows up in the profiler, cache it:

const visible = useMemo(
  () => products.filter(p => matches(p, query)),
  [products, query]
);
JSX

useMemo is still derivation; it just skips recomputing when inputs have not changed. For a few hundred items a plain calculation usually costs well under a millisecond, and the memo bookkeeping is not worth the noise. With the React Compiler enabled in React 19 projects, this memoisation is often applied for you.

Worked scenario

A dashboard shows a table with a “select all” checkbox. The first version kept selectedIds in state and also allSelected in state, synced with an effect:

const [selectedIds, setSelectedIds] = useState(new Set());
const [allSelected, setAllSelected] = useState(false);

useEffect(() => {
  setAllSelected(selectedIds.size === rows.length);
}, [selectedIds, rows]);

function toggleAll() {
  setAllSelected(!allSelected);
  setSelectedIds(allSelected ? new Set() : new Set(rows.map(r => r.id)));
}
JSX

Bugs followed. After new rows loaded, the checkbox stayed ticked for one frame with fewer rows selected. Clicking “select all” during that frame inverted the wrong way. A later change made rows a fresh array on every parent render, and the effect ran after every render.

The fix removes the redundant state:

const [selectedIds, setSelectedIds] = useState(new Set());
const allSelected = rows.length > 0 && rows.every(r => selectedIds.has(r.id));

function toggleAll() {
  setSelectedIds(allSelected ? new Set() : new Set(rows.map(r => r.id)));
}
JSX

allSelected is now always correct for the current render, including the moment new rows arrive.

Common mistake

  • “I need state so the component re-renders when it changes.” A derived value changes only when its inputs change, and those inputs already trigger renders.
  • Initialising state from props and expecting it to follow. useState(props.value) reads the prop once. If you want a reset when an identity changes, use a key to reset the component; if you want it to track the prop, do not copy it.
  • Wrapping every calculation in useMemo. That adds complexity and dependency arrays to maintain without a measured benefit.
  • Effect chains. One effect setting state that triggers another effect is a strong smell of derived state, and a common route to maximum update depth exceeded.

Verify the behavior

Count renders in both versions. Add console.log('render', query, visible.length) at the top of the component body (after computing visible) and type one character:

Effect version:   render a 120   <- stale list committed
                  render a 14
Derived version:  render a 14
Text

The effect version logs two renders per keystroke and the first shows the stale count. The derived version logs one render with matching values. (In development under Strict Mode each line may appear twice; the ratio is what matters.)

Follow-up questions

When is copying a prop into state correct? When it is genuinely an initial value the user then edits independently, like a draft. Name it initialX to signal that it is read once.

How is derived state different from the old getDerivedStateFromProps? That class lifecycle let you copy props into state; the React docs have long recommended avoiding it in favour of computing values or using keys.

Where should I compute values that depend on server data? Same place: render. Fetching belongs in a data layer or effect, but shaping the fetched data does not need its own state.

Is it fine to sort in render? Yes, but sort a copy ([...rows].sort(...) or rows.toSorted(...)) so you do not mutate props.

Interview exercise

A component receives user with firstName and lastName. It stores fullName in state and updates it in an effect. Name two concrete bugs this design can cause and rewrite it.

Answer and reasoning

First, when user changes, the first render shows the previous user’s full name, because the effect only runs after commit, so there is a visible flash of wrong data. Second, if anything else ever calls setFullName (for example an edit field), the stored value diverges from user and nothing reconciles them. It also costs an extra render each time. The rewrite deletes the state and the effect: const fullName = `${user.firstName} ${user.lastName}`;. The string concatenation is trivial, so no useMemo is warranted.

Continue learning

Continue with the React interview questions and the React MCQs. Related notes: effect dependencies, resetting with a key and memo, useMemo and useCallback. Official reference: Choosing the State Structure and You Might Not Need an Effect on react.dev.

More in React

read ✓React · mid

React StrictMode and Double-Invoked Effects

What StrictMode calls twice in development, the exact mount, unmount, mount sequence in React 19, and how to fix the effects it exposes.

~6 min readread →
esc