A teammate wraps every event handler in a React component in useCallback to "reduce re-renders". When does stabilising a handler's identity actually prevent a render, and when does it change nothing?
answer
- identity matters only where compared
- memo, dependency arrays, derived values
- unmemoized children re-render regardless
- one inline prop defeats the whole bail-out
- the compiler does this at build time
basics
~20 sA stable handler prevents a render only where something compares it: a child wrapped in React.memo, or a dependency array. Passed to an ordinary child it changes nothing, because that child re-renders whenever its parent does.
solid answer
~50 sIdentity matters only where code compares it. There are three such places: the shallow prop comparison inside `React.memo`, dependency arrays of `useEffect`, `useMemo` and `useCallback`, and any value derived from the handler that is itself compared, such as a context value. Give a stable handler to a plain child and nothing improves — an unmemoized child re-renders whenever its parent renders, whatever its props look like. So `useCallback` only pays when it is *paired* with a consumer that compares, and the pairing has to be complete: if the same memoized child also receives `style={{...}}` or `items={items.filter(...)}` inline, that prop breaks the comparison and the stabilised handler is wasted. It also depends on `useCallback`'s own dependencies being stable; if one changes every render, the memoized callback does too. In React 19, the React Compiler does this at build time for code that follows the Rules of React.
code
javascript · 19 linesimport { memo, useCallback, useState } from 'react';
const Row = memo(function Row({ label, onPick }) {
return <button onClick={onPick}>{label}</button>;
});
function List() {
const [query, setQuery] = useState('');
const onPick = useCallback(() => console.log('picked'), []);
return (
<>
<input value={query} onChange={e => setQuery(e.target.value)} />
<Row label="one" onPick={onPick} />
</>
);
}
export default List;go deeper
Know that useCallback keeps a function the same object between renders, and that it does not by itself make an application faster — something has to be comparing that function.
Explain the pairing: memo's shallow prop comparison and dependency arrays are the places identity is read, and an unmemoized child re-renders with its parent no matter what its props look like.
Demonstrate the diagnosis — a sibling inline prop or a churning dependency silently voids the memoization — and be willing to say that most handlers in a normal component should not be wrapped at all.
Own the policy: whether the team hand-memoizes, whether the React Compiler replaces that practice, and how you stop "wrap every handler" from becoming an unexamined convention that costs maintenance and hides stale-value bugs.
## The claim to interrogate "Wrap handlers in `useCallback` to reduce re-renders" is not a rule, it is half of one. `useCallback` does exactly one thing: it returns the same function object across renders while its dependencies compare equal. Returning the same object prevents a render only if something in the system was going to compare that object and act on the difference. Nothing else about `useCallback` is a performance feature. ## Where a stable identity actually pays **A memoized child.** `React.memo` wraps a component so React shallowly compares the new props with the previous ones and skips re-rendering when every prop is unchanged. A handler prop that is a new object each render makes that comparison fail every time, so the memo does nothing but pay for a comparison. Stabilise the handler and the child can bail out. ```jsx const Row = memo(function Row({ label, onPick }) { /* ... */ }); function List() { const [query, setQuery] = useState(''); const onPick = useCallback(() => pick(), []); // typing in the input re-renders List but not Row return <><input value={query} onChange={e => setQuery(e.target.value)} /><Row label="one" onPick={onPick} /></>; } ``` **A dependency array.** A handler listed in `useEffect`'s dependencies re-runs the effect whenever its identity changes. If the effect subscribes, opens a socket, or starts a timer, an unstable handler turns setup and teardown into a per-render loop — a correctness and resource problem, not just a slow one. Here stabilising is not an optimisation, it is the fix. **A value built from it.** A context value object such as `{ user, signOut }` is recreated whenever `signOut` changes identity, and every consumer of that context re-renders. Stabilising the handler is a precondition for stabilising the object. ## Where it changes nothing **An ordinary child.** React re-renders a child because its parent rendered, not because its props changed. Without `memo`, prop identity is never consulted, so a stabilised handler saves precisely zero renders. This is the single most common wasted `useCallback` in real codebases. **A DOM element.** `<button onClick={stable}>` gains nothing from stability: React updates the stored handler either way and performs no DOM work for the change. **A child that is memoized but also receives an unstable prop.** Shallow comparison is all-or-nothing. One inline object, array, or freshly filtered list defeats the bail-out and the carefully stabilised handler alongside it is dead weight. Memoization has to be applied to the whole prop surface of a boundary or not at all — which is why it is better thought of as a property of a boundary than of a callback. **A `useCallback` whose own dependencies churn.** `useCallback(fn, [items])` where `items` is `data.filter(...)` computed inline returns a new function every render, because its dependency is new every render. You have added a hook and changed nothing. Chasing this backwards through a component is how memoization spreads until half the file is hooks. ## What it costs to do it everywhere Each call adds a hook slot, a dependency array that must stay honest, and a retained closure that lives as long as the component. The dependency array is the real liability: it is a second place to state the truth about what the handler reads, and when it is wrong you get the stale-value class of bug rather than a slow app. Applying it reflexively converts a cheap, correct component into a more expensive, more fragile one. ## React 19 and the compiler The React Compiler (`babel-plugin-react-compiler`) performs this memoization at build time, inserting caching for values and functions based on what a component actually reads, with the Rules of React as its correctness precondition and `"use no memo"` as the per-component escape hatch. Where it is enabled, hand-written `useCallback` for identity stability is largely redundant, and the conversation shifts from "which handlers do I memoize" to "is this component compiler-safe". You still need the mechanism in your head: to read the enormous body of existing manually-memoized code, and to reason about boundaries the compiler cannot see through. ## How to answer this in an interview Say the pairing rule first — identity matters only at a comparison boundary — then name the boundaries, then name the two ways the pairing silently fails (a sibling unstable prop, an unstable dependency). Finishing with "and in React 19 the compiler mostly makes this a build-time concern" shows you know where the practice is heading rather than only where it has been.
- A memoized child receives a stable handler and still re-renders on every parent render. What do you check first?The other props. Shallow comparison is all-or-nothing, so a single inline object, array literal, or freshly computed list breaks the bail-out and makes the stabilised handler irrelevant. Compare the previous and next props pairwise and find the one that differs — it is almost always an inline literal or a derived array that nothing memoized.
- When is stabilising a handler about correctness rather than performance?When the handler is a dependency of an effect. An unstable identity makes the effect tear down and set up on every render, which restarts subscriptions, timers and requests in a loop — a resource and behaviour bug, not a slow render. In that situation you either stabilise the handler or restructure so the effect does not depend on it.
- Does the React Compiler make useCallback obsolete?For identity stability in components that follow the Rules of React, largely yes — the compiler inserts equivalent caching at build time, and `"use no memo"` opts a component out. It does not remove the need to understand the model: you still read and maintain manually memoized code, and you still reason about boundaries where the compiler cannot prove safety.
saying these in an interview costs you the question
- Believes useCallback stops re-renders on its own
- Memoizes handlers for children that are not wrapped in memo
- Ignores that a sibling inline prop breaks the memo comparison
- Never checks whether useCallback's own dependencies are stable
- Treats memoization as free rather than as a maintained dependency array