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.
| piece | role | analogy |
|---|---|---|
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
| intent | pattern | why |
|---|---|---|
| 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 |