skip to content

In React, what is a callback ref, and what does React pass to that function when the element mounts and when it unmounts?

level: juniorimportance: should knowfreq 52%

answer

  1. a function, not a slot
  2. React calls it, you do not read it
  3. node on attach, null on detach
  4. React 19 lets it return cleanup
  5. identity change re-runs the pair

basics

~20 s

A callback ref is a function passed as an element's ref prop instead of a ref object. React calls it with the DOM node when the element attaches, and with null when it detaches, so you can run code at the exact moment a node appears or disappears.

solid answer

~50 s

Instead of passing a `useRef` object, you can pass a function as `ref`. React invokes that function during commit with the DOM node as its only argument when the element attaches, and invokes it again with `null` when the element is removed or the ref changes. That gives you a notification, not just a slot: you can focus the node the instant it appears, store it in a `Map`, or wire something up on attach and undo it on the `null` call. In React 19 a callback ref may instead return a cleanup function, and when it does React runs that cleanup on detach rather than calling the function with `null`. Callback refs are the right tool for conditionally rendered elements and for lists, where an object ref would silently give you nothing when the element is absent.

go deeper

for a junior

Be able to say a callback ref is a function you put in the ref prop, that React calls it with the DOM node when the element appears and with null when it goes away, and give one use such as focusing an input.

for a middle

Explain the identity rule: React compares the ref prop between renders, so a new inline function each render causes a detach/attach pair. Know the React 19 cleanup-return form and when you would use useCallback instead.

for a senior

Show judgment about what belongs in a callback ref versus an effect, and be alert to setState inside one causing an attach loop. Explain why lists and conditional elements force the callback form.

for a principal

Own the convention for a codebase: where imperative node access is allowed at all, whether callbacks return cleanups uniformly now that React 19 supports it, and how you keep ad-hoc DOM access from leaking through a component library's public API.

## Two ways to spell `ref` The `ref` prop on a host element accepts two things: 1. **A ref object** — typically from `useRef(null)`. React writes the node into its `.current` property. This is a *slot*: you can look in it later, but nothing tells you when it was filled. 2. **A function** — the callback ref. React *calls* it. This is a *notification*: your code runs at the exact moment the node attaches or detaches. ```jsx function AutoFocusInput() { return <input ref={(node) => { if (node) node.focus(); }} />; } ``` ## The call contract During commit, when React attaches the element, it calls your function with the DOM node: ``` yourCallback(domNode) ``` When React removes the element from the DOM, or when the value of the `ref` prop itself changes to a different function, it detaches the old one. In React 18 and earlier that detach is always a second call with `null`: ``` yourCallback(null) ``` So the canonical shape of a callback ref is a branch on the argument: ```jsx const measureRef = (node) => { if (node) { // attached } else { // detached } }; ``` The timing is the same as for object refs: this happens in the commit phase, before that commit's layout effects and effects run. The node you receive is a live DOM element, fully inserted into the document. ## React 19: returning a cleanup React 19 added a second, cleaner shape. A callback ref may return a function, and React treats that returned function as the cleanup for the attachment: ```jsx const rowRef = (node) => { registry.add(node); return () => registry.delete(node); }; ``` When you return a cleanup, React runs it on detach **instead of** calling your function again with `null`. The pattern mirrors `useEffect`'s setup/cleanup shape, and it removes the awkward `if (node)` branch along with the risk of forgetting the null case entirely. One practical consequence: because the return position now means something, you should give the callback a block body rather than a concise arrow that accidentally returns a value. `ref={(node) => (map[id] = node)}` returns the node, which is not a valid cleanup; write `ref={(node) => { map[id] = node; }}` instead. TypeScript's React 19 types reject the implicit-return form for exactly this reason. ## Why you would choose a callback ref over an object ref **Conditionally rendered elements.** If an element is behind a condition, an object ref is `null` whenever it is absent and you have no way to know when it came back. A callback ref fires the moment it appears. ```jsx {isOpen && <dialog ref={(node) => node?.showModal()} />} ``` **Lists.** You cannot call `useRef` in a loop — hooks must be called unconditionally at the top level — so you cannot create one ref per row that way. A callback ref closed over the row's id can put each node into a shared `Map`. **Doing work at attach time.** Anything that must happen at the instant a node exists — and be undone when it goes away — reads naturally as a callback ref, because the attach and the detach are the same piece of code. ## The gotcha every interviewer follows up on Because React compares the `ref` prop by identity between renders, an inline arrow function is a *new* callback on every render, so React detaches the old one and attaches the new one on every re-render. That is usually harmless for a one-line `node?.focus()`, and genuinely wasteful when the callback does real work. The fix is to give the callback a stable identity with `useCallback`, or to accept the churn deliberately when the callback is trivial. ## What a callback ref is not It is not an effect. It runs during commit, it receives one argument, and its lifetime is tied to *this element's attachment*, not to the component's mount. And it is not a place to call `setState` unconditionally: doing so on every attach can loop, since a re-render that changes the callback's identity re-triggers the attach.

  • When would you reach for a callback ref instead of a useRef object?
    When you need to know *when* the node attaches rather than just look at it later: a conditionally rendered element, where an object ref gives no notification when it reappears; a list, where you cannot call useRef per row and instead store nodes in a Map keyed by id; or any setup that must be undone on detach, which reads naturally as one attach/cleanup pair.
  • What happens if a callback ref calls setState every time it is invoked?
    You risk an infinite loop. The state update re-renders the component, which can produce a new callback identity, which makes React detach and re-attach and call it again. Either give the callback a stable identity with useCallback, or guard the update so it only fires when the value actually changed.

saying these in an interview costs you the question

  • Says React passes the React element, not the DOM node
  • Forgets that React also calls it with null on detach
  • Thinks a callback ref runs on every render like an effect
  • Claims callback refs were removed in favour of useRef
  • Assumes returning a value from it is always harmless

context