A teammate argues that filtering and sorting a 5,000-item list on every React render is wasteful and wants to keep the sorted result in useState, updated by an effect whenever the inputs change. How do you respond, and when is useMemo the right tool instead?
answer
- measure before you model around speed
- same work, plus an extra render
- useMemo caches, state duplicates
- a cache is not a guarantee
- 5,000 DOM nodes is the real cost
basics
~20 sStoring the result adds a second source of truth, a stale frame and an extra render on top of the same computation, so it is strictly worse. Measure first; if the derivation is genuinely hot, use useMemo, which caches within the render and keeps one source of truth.
solid answer
~50 sThe proposal pays the cost it is trying to avoid and adds new ones. The effect runs after React commits, so the first frame is built from the previous sorted list, and correcting it schedules a second render — the same sort, plus an extra pass, plus a window where the copy disagrees with its inputs. So the state-and-effect version is never the performance answer. I'd measure before doing anything: profile the component and check whether the sort is actually the cost, because in most apps rendering 5,000 DOM nodes is the real problem and the fix is virtualization, not memoization. If the derivation genuinely dominates, `useMemo` is the right tool: it caches the computed value across renders while it stays a derived value, available in the same render that needs it, with no second copy to go stale. Note that `useMemo` is a cache, not a guarantee — React may discard entries — so correctness must never depend on it.
code
jsx · 26 linesimport { useMemo, useState } from 'react';
function RowList({ rows }) {
const [query, setQuery] = useState('');
// Derived, and cached only because a profile said the sort was hot.
// Still one source of truth: no effect, no second copy, no stale frame.
const visible = useMemo(
() =>
rows
.filter((r) => r.name.includes(query))
.sort((a, b) => a.name.localeCompare(b.name)),
[rows, query],
);
return (
<>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<ul>
{visible.map((r) => (
<li key={r.id}>{r.name}</li>
))}
</ul>
</>
);
}go deeper
Know that a value computed during render from state or props does not need to be stored, and that useMemo caches such a computation rather than turning it into state. Say that you would measure before adding either.
Trace the effect version's timeline — render with the old list, commit, effect sorts, second render — and show that it does the same work plus an extra pass while opening a staleness window. Contrast that with useMemo producing the value inside the render that needs it.
Lead with measurement and separate render volume from derivation cost, since 5,000 DOM nodes usually dominates a 5,000-item sort. Justify memoisation from a profile, and note that useMemo is a discardable cache correctness must not rely on.
Set the team default: derive by default, memoise only from evidence, and never mirror into state. Point out that plainly-derived code is what lets build-time auto-memoisation help you, and push large sorting and filtering to the data layer rather than shipping the whole dataset to the client.
## Answer the correctness question before the performance one A derived value computed during render cannot be stale, because it is rebuilt from its inputs every time. Moving it into state converts a structurally-correct design into one that needs synchronisation. Before discussing speed, that trade has to be named: the proposal buys nothing and sells the invariant. Walk through what the effect version actually does when `query` changes: 1. Render with the new `query` but the *old* stored list. 2. Commit; the browser may paint that mismatched frame. 3. The effect runs and sorts — the same work the render-time version does. 4. `setState` schedules a second render, which commits the correct list. So: the same sort, an extra render pass, an extra commit, and a visible window where the displayed rows do not match the filter. Add the maintenance hazards of any mirror — a forgotten dependency freezes the list, a second writer races the effect — and it is worse on every axis, including the one it was proposed to improve. ## Then ask whether the cost is real "5,000 items" sounds expensive and usually is not. A `filter` plus `sort` over 5,000 plain objects is on the order of a millisecond or two on a modern machine. Rendering 5,000 rows into the DOM is not — that is tens of milliseconds of reconciliation, layout and paint. Measure before optimising: record an interaction in the React DevTools Profiler and see which component owns the time, or bracket the derivation with `performance.now()` and look at real numbers on the slowest device you support. Most of the time the answer is that the derivation is noise and the render volume is the problem, which points at rendering fewer rows (windowing) or splitting the component so the expensive subtree does not re-render — not at caching the array. ## When `useMemo` is the right tool If the profile says the derivation itself dominates, `useMemo` is what you want: ```jsx const visible = useMemo( () => rows.filter((r) => r.name.includes(query)) .sort((a, b) => a.name.localeCompare(b.name)), [rows, query], ); ``` The critical difference from the `useState` proposal: this is still derived state. The value is produced *during* the render that needs it, so there is no stale frame and no extra render; the inputs are declared in one place; and there is no second writer. `useMemo` is a cache over a pure computation, not a copy of a fact. Two caveats worth stating unprompted. First, `useMemo` is a performance hint, not a semantic guarantee — React is allowed to throw cached values away and recompute, so code must stay correct if the function runs on every render. Never put a side effect, or an identity something depends on staying stable forever, inside it. Second, memoisation is not free: it costs a dependency comparison and retains the previous result in memory, and if the dependencies change on every render it is pure overhead. A second legitimate reason to memoise a derived array is *referential stability* rather than raw compute — a new array identity each render defeats downstream bail-outs — but that is a rendering-cost argument, and it should also be made from a profile rather than by reflex. ## React 19 and the compiler With the React Compiler (`babel-plugin-react-compiler`) enabled, this class of memoisation is inserted at build time from the component's data flow, and hand-written `useMemo` for ordinary derived values becomes largely unnecessary. That reinforces the argument rather than weakening it: the compiler can memoise a value derived during render because it can see the dependency graph; it can do nothing for a copy you have hidden in `useState` behind an effect. Writing plainly derived code keeps the automatic option open. ## When precomputing genuinely belongs elsewhere There are cases where the sorted list should not be computed in the component at all — the server can return it ordered, or the ordering is a query the data layer should own. That is a different conversation from `useState` versus `useMemo`, and it is often the best answer for large datasets: don't ship 5,000 rows to the client to sort them. ## How to close the discussion "Derived by default; measure; memoise the derivation if the measurement says so; never mirror it into state." The proposal is not a smaller version of `useMemo` — it is a different thing that happens to sound similar, and the difference is that one keeps a single source of truth and the other does not.
- Your teammate says the effect version is fine because the user never notices one stale frame. What do you say?Perception is the weakest of the objections. The durable problems are structural: a second source of truth a future edit can desynchronise, a dependency array that silently freezes the list when someone adds an input, and an extra render on every keystroke. And on a slow device the frame is noticeable — exactly where the optimisation was supposed to help.
- Can correctness ever depend on useMemo not recomputing?No. React treats it as a cache it may discard, so the function must be pure and safe to run on any render. If you need a value guaranteed stable for the lifetime of the component — an object identity something else keys on, or a mutable instance — that is a different requirement and `useMemo` is the wrong tool for it.
- With the React Compiler enabled, is this discussion obsolete?The `useMemo` half largely is: the compiler inserts memoisation from the data flow, so hand-written memos for ordinary derived values become redundant. The modelling half is not. The compiler can only optimise values actually derived during render; a copy hidden in state behind an effect is opaque to it, so the plain derived form is what keeps the automation available.
saying these in an interview costs you the question
- Storing the result in state avoids recomputing it
- useMemo guarantees the value is never recomputed
- Wrap every derived value in useMemo just in case
- Recomputing on every render means the work happens twice
- Sorting 5,000 objects is obviously the bottleneck, no need to measure