skip to content

In React 19, what may a callback ref return, and how does returning it change the way React detaches the ref?

level: middleimportance: nice to knowfreq 30%

answer

  1. object ref or function ref
  2. the old second call was null
  3. now it can return a teardown
  4. return one and null is skipped
  5. concise arrow bodies return by accident

basics

~20 s

A React 19 callback ref may return a cleanup function. When it does, React runs that cleanup on detach instead of calling the callback a second time with null, giving refs the same attach/cleanup shape that effects already have.

solid answer

~40 s

A `ref` prop accepts either a ref object from `useRef` or a function. React calls the function with the attached element, and historically signalled detachment by calling the same function again with `null` — so cleanup code lived in an `if (node) ... else ...` branch. React 19 lets the callback return a cleanup function instead: `ref={(node) => { const obs = observe(node); return () => obs.disconnect(); }}`. If you return one, React calls it on detach and **does not** call your callback with `null`. One migration trap comes with it: a concise arrow body like `ref={(el) => (inputRef.current = el)}` implicitly returns the assigned value, which React 19 now interprets as a return value where a cleanup function is expected. The fix is a block body with braces so nothing is returned.

code

javascript · 11 lines
javascript
export function AutoResize({ onResize }) {
  return (
    <div
      ref={(node) => {
        const observer = new ResizeObserver(onResize);
        observer.observe(node);
        return () => observer.disconnect();
      }}
    />
  );
}

go deeper

for a junior

Know that ref accepts a function as well as a useRef object, and that React calls the function with the element. Remember that React 19 lets that function return a cleanup.

for a middle

Explain the protocol change: the old callback was called a second time with null, and a returned cleanup now replaces that call. Be able to spot the concise-arrow implicit return that this change turns into a bug.

for a senior

Choose deliberately between the two ref shapes: callbacks when attachment is the event worth reacting to, objects when a handler simply needs the node later. Show the cleanup-scoping benefit over the old if/else branch.

for a principal

Frame it as API convergence — refs, effects and callback refs now share one setup-returns-teardown shape, which lowers the cost of reading unfamiliar code. Weigh that against a migration whose failure mode is a silently mistyped one-liner across a large codebase.

## Two shapes for a ref prop Anywhere React accepts `ref`, you may pass either of two things: 1. **A ref object** — what `useRef` returns. React writes the element into its `current` property and writes `null` back on detach. You do not get a notification; you just read the box later. 2. **A callback** — a function React calls *with* the element. This is the shape to choose when attachment itself is the event you care about, because it runs at the moment the node becomes available rather than leaving you to check later. ## The old two-call protocol Before React 19, a callback ref was called twice around its lifetime: once with the node, once with `null` when the element went away. Every callback that set something up therefore carried a branch: ```jsx <div ref={(node) => { if (node) { resizeObserver.observe(node); } else { resizeObserver.disconnect(); } }} /> ``` It worked, but it read badly. The setup and the teardown sat in the same function with no shared scope for whatever the setup produced, so anything the cleanup needed had to be hoisted into a ref or a module variable. ## The React 19 cleanup return React 19 lets the callback return a function, and treats that returned function as the cleanup: ```jsx <div ref={(node) => { const observer = new ResizeObserver(handleResize); observer.observe(node); return () => observer.disconnect(); }} /> ``` The teardown now closes over exactly what the setup created, which is the same ergonomic win `useEffect` has always had. Two behavioural points follow: - **The null call is skipped.** If you return a cleanup, React will not invoke your callback again with `null`. That is deliberate: the two mechanisms would otherwise both fire and double-tear-down. - **The old protocol still works.** If you return nothing, React keeps calling with `null` on detach, so existing callbacks are untouched. ## The implicit-return trap The most common React 19 upgrade snag is a one-liner that was never meant to return anything: ```jsx <input ref={(el) => (inputRef.current = el)} /> ``` An arrow with a concise body returns the value of its expression, and an assignment evaluates to the assigned value — so this returns the element. React 19 expects a returned value to be a cleanup function, and the React 19 types now reject the pattern to stop you shipping it. The fix is purely syntactic: wrap the body in braces so the arrow returns `undefined`. ```jsx <input ref={(el) => { inputRef.current = el; }} /> ``` Worth internalising as a general lesson about concise arrow bodies in any position where the return value has acquired a meaning. ## When to prefer a callback over a ref object Reach for a callback when you need to *react* to attachment rather than merely hold the node: wiring an observer, registering a node in a map keyed by id, measuring on attach, handing a node to a third-party constructor. Reach for a `useRef` object when you only need to reach the node later from a handler — it is simpler and there is nothing to clean up. ## Interaction with useImperativeHandle The two compose without special handling. A child that installs a handle with `useImperativeHandle` does not care whether the parent passed an object ref or a callback; React resolves the handle to whichever shape came in, so a parent's callback ref receives the handle object rather than a DOM node.

  • If a callback ref returns a cleanup, does React still call it with null on unmount?
    No. Returning a cleanup opts you out of the null call entirely — React runs the returned function instead. Mixing both would tear down twice, so the behaviours are exclusive. Callbacks that return nothing keep the old two-call protocol unchanged.
  • When would you choose a callback ref over a useRef object?
    When attachment itself is the event: wiring an observer, registering nodes in a map keyed by id, or handing a node to a library constructor. A ref object only lets you read the node later, which is enough when a handler is what needs it and there is nothing to tear down.
  • Why does <input ref={(el) => (inputRef.current = el)} /> become a problem in React 19?
    The concise arrow body returns the assigned element, and React 19 reads a returned value as a cleanup function. The React 19 types reject it for that reason. Wrap the body in braces so the callback returns undefined and the classic null-on-detach behaviour applies.

saying these in an interview costs you the question

  • Says a ref prop must always be a useRef object
  • Thinks the cleanup return and the null call both fire
  • Returns an assignment expression from a concise arrow ref
  • Believes callback refs were removed in React 19
  • Treats the returned cleanup as running on every render

context