forwardRef & useImperativeHandle

[check: …]
📖 guide (.md) ← react deep dive
📖 Pair this live React playground with the companion guide (.md) — this page is the rendered ground truth. ↗ Refs give DOM access — useRef & DOM access shows a parent reaching its OWN nodes; forwardRef passes that access to a parent.

ref is special: forwardRef unwraps it

ref is not a normal prop — React intercepts it before your component ever sees it (in React 18 and earlier, function components silently drop it). React.forwardRef wraps your component so the parent's ref arrives as the second argument: (props, ref) => {'<'}element ref={ref}/{'>'}. useImperativeHandle then narrows what that ref exposes — instead of the raw DOM node, you return a custom API object. The parent calls inputRef.current.focus() and never knows there's an <input> underneath.

pieceroleanalogy
React.forwardRef(fn) wraps fn(props, ref) so ref is forwarded in the mail slot — passes the parent's ref key into the child's hand
ref (2nd arg) the parent-supplied ref object/callback the key the parent hands over — the child decides what it unlocks
useImperativeHandle defines ref.current = factory() instead of the DOM node the lobby reception — only the methods you list get announced
ref.current (parent side) the custom API object, NOT the DOM node the concierge — you ask .focus(), never enter the back office

1 · the child you write (edit me)

2 · Babel compiles JSX → element tree

<FancyInput ref={inputRef}/> becomes a call where React routes the ref through forwardRef's wrapper into the render function as the 2nd arg. useImperativeHandle then overwrites ref.current with the factory's return.

// (hit "compile & render" to see Babel's output)

3 · live React (the forwarded ref, proven)

The parent holds inputRef and calls into the child's custom API. The gold-check exercises every method automatically: focus() (asserts the input is document.activeElement), sets the DOM value + getValue() (asserts the parent read it), then clear() (asserts the input was emptied). All four prove the child exposed a deliberate ref API — not its DOM node.

ref api: · gold: focus → set value → getValue → clear → expect empty

intent → pattern

intentpatternwhy
let parent focus my DOM forwardRef((props, ref) => <input ref={ref}/>) raw passthrough — parent owns the node directly
expose a curated API useImperativeHandle(ref, () => ({'{focus, clear}'})) encapsulate internals — parent can't break your markup
limit what the ref shows return only the methods you want public in the factory DOM node never leaks — the child stays in control
recompute the handle useImperativeHandle(ref, fn, [deps]) 3rd arg = deps array; omit = rebuilt every render (default)
React 19 shortcut function Child({ref}) { … } — ref as a normal prop no forwardRef wrapper; but the old API still works in 19