A React component passes an inline arrow function as an element's ref prop. Why does React detach and re-attach that ref on every re-render, and how would you stop it?
answer
- ref is just a prop
- two identical arrows, two objects
- React compares by identity
- detach then attach, every render
- stabilise with useCallback
basics
~20 sAn inline arrow is a new function on every render, and React compares the ref prop by identity. Seeing a different value, it detaches the old callback and attaches the new one. Give the callback a stable identity with useCallback to stop the churn.
solid answer
~50 sReact treats the `ref` prop like any other prop: on re-render it compares the new value with the old one by identity. An inline arrow function is freshly created each render, so the values never match, and React runs the detach/attach pair — in React 18 that means calling the old callback with `null` and the new one with the node; in React 19, if the old callback returned a cleanup, React runs that cleanup instead. For a trivial callback this churn is harmless. When the callback does real work — registering the node somewhere, wiring up imperative state — it means tearing that work down and redoing it on every render, which is both wasteful and a source of flicker or lost state. The fix is a stable identity: wrap the callback in `useCallback` with the values it genuinely depends on, or hoist it out of the component when it closes over nothing.
go deeper
Know that writing a function directly inside JSX creates a new function each render, and that React notices the change. Recognise the symptom: a ref callback that runs with null and then the node again on an unrelated update.
Explain identity comparison on the ref prop and the resulting detach/attach pair, and show the useCallback fix with an honest dependency list. Say when the churn is genuinely harmless.
Judge when to fix it: churn matters when the callback owns registration or imperative setup, and it can create an attach/setState loop. Connect it to the same identity rule that makes inline objects re-trigger effects.
Decide how the codebase handles function identity at large — manual useCallback discipline versus enabling the React Compiler to memoize automatically — and make sure the choice is uniform rather than argued case by case in review.
## The rule underneath the behaviour `ref` is a prop, and React reconciles props by comparing old and new values. For a callback ref the value *is* the function. Two arrow functions with identical source text are still two different objects, so a callback written inline in JSX is a new value on every render, and React concludes the ref changed. When the ref value changes, React must detach the old one and attach the new one, because the old callback might be holding the node and the new one has never seen it. So on every re-render you get a pair of calls, not one: ```jsx // new function object on every render <div ref={(node) => console.log('ref call', node)} /> ``` Re-rendering this component logs `ref call null` followed by `ref call <div>`, every time — even though the DOM node itself never changed. In React 19, if the callback returned a cleanup function, that cleanup runs in place of the `null` call, but the pair is still a pair. ## When it is harmless and when it hurts **Harmless.** `ref={(node) => node?.focus()}` runs an extra focus on a node that is already focused; nobody notices. Most one-liners in application code fall here, and chasing the churn is premature optimisation. **Harmful.** The moment the callback owns something, the churn destroys and recreates it on every unrelated re-render: - Registering the node in a shared registry and unregistering on detach: every render removes and re-adds an entry, and anything watching that registry sees spurious churn. - Doing imperative setup on attach: any state that setup accumulated is thrown away. - Triggering a `setState` on attach: now you have a render that causes an attach that causes a render. ## The fix: a stable identity `useCallback` returns the same function object across renders as long as the dependencies are unchanged: ```jsx function Row({ id, registry }) { const rowRef = useCallback((node) => { if (node) registry.set(id, node); else registry.delete(id); }, [id, registry]); return <li ref={rowRef}>…</li>; } ``` Now the ref value only changes when `id` or `registry` changes — which is exactly when you *want* a detach/attach, because the callback's meaning genuinely changed. In React 19 the same callback reads better with a cleanup return: ```jsx const rowRef = useCallback((node) => { registry.set(id, node); return () => registry.delete(id); }, [id, registry]); ``` If the callback closes over nothing from the component, hoist it to module scope; a constant is the cheapest stable identity there is. ## Why not just ignore it Two reasons an interviewer is probing for. First, **it is a diagnosis skill**. "My ref callback fires twice with null in between and I never touched the DOM" is a real bug report, and the answer is identity, not React being unpredictable. It is the same mechanism behind a `useEffect` re-running because an inline object or function landed in its dependency array — one rule, several symptoms. Second, **it interacts with the attach loop**. If the callback calls `setState`, the render caused by that state change creates a new callback, which detaches and re-attaches, which calls `setState` again. With a stable callback the loop cannot start, because the second render leaves the ref value untouched. ## What a stable identity does *not* fix Stabilising the callback stops churn caused by *re-rendering*. It does not stop the detach/attach that happens when the element itself unmounts and remounts — for example when a conditional flips, or when a list item's key changes and React treats it as a different element. Those are real attachment changes and the callback is supposed to fire. It also does not paper over a callback that closes over stale values. The dependency list you give `useCallback` has to be honest: if the callback reads `id`, `id` belongs in the list, otherwise the registry ends up keyed by a value from an earlier render. ## The React Compiler angle Where the React Compiler is enabled, it can memoize functions like this automatically as part of its build-time work, which removes much of the manual `useCallback` ceremony. That does not change the underlying rule — React still compares the ref prop by identity — it only changes who is responsible for keeping the identity stable.
- Does the same churn happen when you pass a useRef object inline instead of a function?No. `useRef` returns the same object on every render, so the ref prop's identity is unchanged and React has nothing to detach. That stability is exactly why the object form is the default; you reach for a callback only when you need to be notified about attachment, and then you have to manage its identity yourself.
- When should you deliberately let a callback ref change identity between renders?When the callback's meaning depends on a value that changed — a row id, a registry instance, a subscription target. Then the detach/attach pair is correct: the old registration was keyed by the old value and must be removed. Put those values in the useCallback dependency list and let React do the swap.
saying these in an interview costs you the question
- Says React compares ref callbacks by their source code
- Blames StrictMode for every double ref invocation
- Thinks useCallback on a ref is always required
- Wraps the callback in useMemo returning it and calls that equivalent-but-different
- Assumes stabilising the ref also stops unmount/remount attach calls