A team proposes a codebase-wide rule for a React 19 app: every component wrapped in React.memo and every callback wrapped in useCallback, enforced in code review. How would you evaluate that proposal?
answer
- blanket rules assume zero cost
- many wrappers can never fire
- dependency arrays are a bug surface
- measure the boundary that dominates
- policy should be falsifiable
basics
~20 sReject it as a blanket rule. Memoizing everything adds comparisons, retained caches and dependency arrays across the whole codebase, and most of those wrappers never skip anything. Target memoization at boundaries where profiling shows a real cost.
solid answer
~50 sI would push back, and with mechanism rather than slogans. A blanket rule assumes memoization is free, and it is not: every wrapper adds a props comparison per render, every `useCallback` adds an allocation and a dependency comparison, and every dependency array is a small correctness surface that can drift and produce stale behaviour. Worse, most of the wrappers cannot even fire — `useCallback` only pays off when the receiving component is memoized and *all* its other props are stable too, which a blanket rule does not establish. The cost that actually shows up is human: reviewers arguing about dependency arrays instead of about the code, and a codebase where nobody can tell which wrappers are load-bearing. I would replace it with a targeted policy — memoize where a profile shows a real cost, name the reason in the PR, and prefer restructuring when it removes the need entirely.
go deeper
Understand that adding React.memo or useCallback everywhere is not automatically better, and that each one has a cost even when it never skips a render.
Be able to explain why a useCallback buys nothing unless the receiving component is memoized and its other props are stable, and why that makes blanket rules ineffective.
Argue from measurement: profile the slow interaction, memoize the boundary that dominates, and treat drifting dependency arrays as a correctness risk you are spreading codebase-wide.
Own the decision procedure, not the verdict — an interaction budget, a required justification per wrapper, a removal path, and an honest assessment of whether tooling should apply this instead of reviewers.
## Why the proposal is attractive The argument for blanket memoization is genuinely reasonable on its face: individual wrappers are cheap, deciding case by case is expensive, and a uniform rule removes debate. Teams usually propose it after a specific incident — a laggy list, a form that stutters — and generalise the fix. Treat the motivation as legitimate and argue about the mechanism, not the intent. ## What the rule actually buys and costs **Per-render cost.** Each `React.memo` wrapper performs a shallow per-prop comparison on every parent render. Each `useCallback` allocates a function and a dependency array every render and compares the dependencies. These are individually small — but a blanket rule applies them to thousands of call sites, most of which would never have re-rendered expensively in the first place. **Wrappers that cannot fire.** This is the sharper argument. `useCallback` only produces a benefit if the callback's identity is consumed by an equality check downstream — a memoized child, or another hook's dependency array. Wrap a callback that is passed to an unmemoized child and the child re-renders anyway; the `useCallback` is pure overhead. Symmetrically, `React.memo` on a child that also receives an inline object, array or JSX children prop can never bail out, so it is a comparison guaranteed to fail. A rule that mandates both without demanding that *every* prop on the boundary be stable produces a large population of wrappers that do nothing. **Retention.** Memoized values and callbacks are held per component instance until dependencies change or the component unmounts. Applied across a large mounted tree, that is a real, if usually modest, shift in the app's memory shape. **Correctness surface.** The durable cost. A dependency array that drifts from what the function reads produces stale behaviour: a handler acting on old state, a derived value that no longer tracks its inputs. That is a bug class you are choosing to spread across the entire codebase in exchange for renders you have not measured. ## What I would propose instead **Measure first, at the boundary.** Profile the interaction that is actually slow, identify the component whose render dominates, and memoize *there*. A handful of deliberate boundaries — the root of an expensive panel, the row component of a long list, a value that many consumers subscribe to — usually captures nearly all of the available win. **Require a stated reason.** Every memoization wrapper carries a one-line justification in the PR: what it prevents and how that was observed. This keeps the population of wrappers small and, crucially, makes them removable later — an unjustified wrapper is one nobody dares delete. **Prefer the fix that removes the need.** Often the real problem is where state lives or how the tree is shaped, and correcting that eliminates the expensive re-render outright rather than paying to skip it. That is strictly better than a wrapper, because there is nothing left to maintain. **Consider automation rather than a review rule.** React 19 ships alongside a compiler that applies memoization at build time; whether to adopt it is its own evaluation, but it is the honest alternative to asking humans to hand-apply a mechanical rule. If a team is convinced that memoization should be universal, the argument points at tooling, not at a review checklist. ## How to run the conversation The failure mode in this discussion is dogma on both sides — "premature optimization" versus "it's basically free". Neither is an argument. Come with numbers from the app's own profile, name the specific wrappers in the current codebase that cannot fire, and propose a policy that is falsifiable: here is the interaction budget, here is how we detect a regression, here is where memoization is warranted today. A principal-level answer ends with a decision procedure the team can apply next month without you in the room.
- Why is a useCallback often pure overhead even when the callback is passed down as a prop?Because identity only matters if something downstream compares it. If the receiving component is not memoized it re-renders regardless, and if it is memoized but also receives an inline object or JSX children prop, the comparison fails on that prop anyway. The stable callback then buys nothing while still costing an allocation and a dependency check each render.
- What would make you accept a broad memoization policy after all?Evidence plus automation. If profiling showed render cost dominating across many similar surfaces — say a data-dense product built from thousands of rows — and the memoization were applied mechanically by tooling rather than by reviewers, the maintenance objection largely disappears. What I would not accept is a human-enforced rule justified only by the belief that wrappers are free.
- How would you keep the team from re-litigating this every quarter?Write down a decision procedure rather than a verdict: an interaction budget, the profiling step that must precede a wrapper, and the requirement that each wrapper states what it prevents. That turns a taste argument into a check anyone can run, and makes unjustified wrappers safe to delete later.
saying these in an interview costs you the question
- Argues memoization is free so blanket use is harmless
- Ignores that most wrappers can never bail out
- Answers only with the slogan 'premature optimization'
- Overlooks dependency arrays as a stale-data bug surface
- Proposes a rule with no measurement or removal path