A React page holds a search box's value in state on the top-level page component and passes it down to a filtered table and a chart. Typing in the box is visibly laggy. What is the likely cause, and how would you confirm it before changing any code?
answer
- state placement equals render scope
- keystroke rate turns cheap into slow
- one expensive render or many trivial ones
- measure a commit before changing code
- memoizing first is the classic wasted fix
basics
~20 sThe state lives too high, so every keystroke re-renders the whole page subtree including the chart and table. Confirm before fixing: profile a single keystroke and check whether the cost is many cheap renders or one expensive component's render body.
solid answer
~50 sThe likely cause is where the state lives rather than the input itself. Each keystroke calls the setter on the page component, and React renders that component and everything beneath it — the table and the chart included — because rendering cascades down by default regardless of whether a child cares about the query. At keystroke rate that cascade can easily miss a frame. Before changing anything I would record a profile while typing a few characters and ask two questions: is a single commit slow, and which component's own render time dominates it? That distinguishes a genuinely expensive render — a table re-filtering ten thousand rows, a chart recomputing scales — from a large number of trivial renders, which usually cost far less than people assume. The fix follows from that answer: narrow where the fast-changing state lives, or make the expensive downstream update non-urgent. Guessing and memoizing first is how you end up with a memoized chart that still re-renders.
go deeper
Know that updating state on a parent re-renders everything below it, so a value that changes on every keystroke should not be stored far above the input.
Explain the mechanism end to end: setter on the page component, top-down cascade through table and chart, once per keystroke, against a frame budget of roughly 16 ms.
Lead with measurement. Separate one expensive render from many trivial ones, choose the fix from what the profile says, and be ready to explain why memoizing first often changes nothing here.
Own the general lesson: state placement is an architectural decision that sets the default render scope, so review guidance should target where fast-changing state lives rather than mandating a memoization layer the team must maintain forever.
## Why the whole page renders at all React renders top-down. When the page component's state changes, React calls the page component again, then the children it returned, then their children, all the way down — unless something along the way opts out. Nothing about a chart component "not using the query" exempts it: it is a child of a component that rendered, so it renders. This is the second of the three re-render triggers (own state, parent rendered, context value changed) and it is by far the most common one behind a laggy screen. The placement of the state is therefore the placement of the render scope. State on the page component means the render scope is the page. State on a small `SearchBox` component means the render scope is the search box. The input element is innocent; the `useState` call sitting three levels above it is what makes every keystroke expensive. ## Why keystrokes in particular A cascade that costs 8 ms is invisible on a button click and unacceptable in a text field. Typing produces events at whatever rate the user types, and each one must render, commit and paint before the character looks like it landed. If the work per keystroke approaches the frame budget of roughly 16 ms at 60 Hz, characters visibly trail the keyboard. The same cascade fired once per navigation would never have been reported as a bug. Frequency is half of the cost equation. ## Confirm before you change code The reason to profile first is that two very different problems produce the same complaint, and they have opposite fixes. **Too much work in one render.** A single component's own render time dominates the commit: the table filters and sorts ten thousand rows in its body, the chart recomputes scales and paths, a date formatter runs per row. Narrowing the render scope may not help much here, because that component genuinely has to run whenever the query changes — the work itself needs to shrink, be cached, or be deprioritised. **Too many trivial renders.** Hundreds of small components each render in microseconds, and no single one stands out. Here the total is what hurts, and the answer is structural: stop the cascade from reaching the parts of the tree that do not depend on the value. Record a profile while typing several characters, then read the per-commit durations and, within the slowest commit, which components account for the time versus their children. Two supporting checks are worth doing at the same time: confirm there is exactly one commit per keystroke rather than several (a stray effect writing state during the update can double them), and check whether the slow work is inside render or inside a layout effect that reads the DOM. One caveat on measurement: development builds are slower than production across the board, and a `<StrictMode>` subtree calls component bodies twice in development. Use development numbers to compare before and after, and a production build for absolute claims. ## The direction of the fix Once the cause is named, the fix follows from it, and the honest ranking puts structure first. If the cascade is the problem, the fast-changing value should live closer to the thing that changes, so that the render scope shrinks to the input and whatever genuinely displays it. If one component's work is the problem, the work needs to be cheaper, or the expensive update needs to be marked as non-urgent so the keystroke itself stays responsive while the heavy result catches up — `useDeferredValue` and `useTransition` exist for exactly that shape of problem. Memoizing the chart is the fix people try first and it frequently does nothing, because the props being passed down were recreated on each render anyway, or because the chart really does depend on the query. ## What the interviewer is listening for They want to hear "where does the state live" as your first hypothesis, "let me measure" as your first action, and a fix that is chosen by the measurement rather than by habit. A candidate who jumps straight to wrapping components in memoization has skipped the diagnosis, and in this exact scenario the memoization is very often wasted work.
- How would you tell whether the problem is one expensive render or many cheap ones, and why does it matter?Look at where the time sits inside a slow commit: one component whose own render time dominates, versus a long tail of components each costing microseconds. It matters because the first needs the work itself to get cheaper or deprioritised, while the second needs the cascade narrowed. The same fix applied to the wrong one achieves nothing.
- The chart is wrapped in memoization and still re-renders on every keystroke. What would you look at?Whether it is receiving a prop whose identity changes each render — an inline object, array or arrow function created in the parent's body — since the comparison is by reference. Also whether the chart reads a context whose value is recreated on every render, which bypasses the props comparison entirely.
- Would debouncing the input fix this?It hides it rather than fixing it, and it makes the field feel delayed because the displayed value lags the keystrokes. The value the user types should update immediately; only the expensive downstream computation should be delayed or deprioritised, which is a different mechanism from debouncing the input itself.
saying these in an interview costs you the question
- Blames the input element rather than where the state lives
- Wraps everything in memoization before profiling anything
- Assumes children without the query prop will not re-render
- Debounces the input so the field itself lags behind typing
- Quotes development-build timings as the app's real cost