In one minute
A parent can pass a ref to a component. By default, if the component puts that ref on an <input>, the parent gets the whole DOM node. useImperativeHandle(ref, createHandle, deps) lets the component hand back its own object instead, with only the methods it chooses, like focus() and clear(). It’s for commands: “focus now”, “scroll now”, “play now”. Those are things that happen once at a moment, not facts the UI should keep showing, so they fit badly in state and props. In React 19, ref is a normal prop for function components, so you don’t need forwardRef.
You’ll be able to: expose a small imperative API from a component, and explain why a “shouldFocus” prop doesn’t work.
1function CouponField({ shouldFocus }) {2 const inputRef = useRef(null)3 useEffect(() => {4 if (shouldFocus) inputRef.current.focus()5 }, [shouldFocus])6 return <input ref={inputRef} />7}89function Checkout() {10 const [shouldFocus, setShouldFocus] = useState(false)11 return (<>12 <CouponField shouldFocus={shouldFocus} />13 <button onClick={() => setShouldFocus(true)}>Apply coupon</button>14 </>)15}- event
<button onClick={() => setShouldFocus(true)}>Apply coupon</button>Click 1: the flag goes fromfalsetotrue. - effects
if (shouldFocus) inputRef.current.focus()The dependency changed, so the effect runs and focuses the field. - event
<button onClick={() => setShouldFocus(true)}>Apply coupon</button>The user clicks elsewhere, then click 2. The flag is alreadytrue: same value, no re-render, no effect. - effectsRecorded: nothing happened on click 2, and focus stayed on the button. A command doesn’t fit in state.
1function CouponField({ ref }) {2 const inputRef = useRef(null)3 useImperativeHandle(ref, () => ({4 focus: () => inputRef.current.focus(),5 clear: () => (inputRef.current.value = ''),6 }), [])7 return <input ref={inputRef} />8}910function Checkout() {11 const couponRef = useRef(null)12 return (<>13 <CouponField ref={couponRef} />14 <button onClick={() => couponRef.current.focus()}>Apply coupon</button>15 </>)16}- event
<button onClick={() => couponRef.current.focus()}>Apply coupon</button>Click 1 calls thefocusmethod the field exposed. focus: () => inputRef.current.focus(),Inside the field, it focuses the real input. No state, no render.- event
<button onClick={() => couponRef.current.focus()}>Apply coupon</button>Click 2 does exactly the same. useImperativeHandle(ref, () => ({Recorded: focused both times. The parent can only use what’s exposed:Object.keys(handle)was["focus", "clear"], andhandle.stylewasundefined.
The example: “Apply coupon”
At checkout, clicking “Apply coupon” with an empty field should put the cursor back in the coupon field, every time.
1. With a prop
function CouponFieldFlag({ shouldFocus }: { shouldFocus: boolean }) {
const inputRef = useRef<HTMLInputElement>(null)
useEffect(() => {
if (shouldFocus) inputRef.current!.focus()
}, [shouldFocus])
return <input id="coupon" ref={inputRef} placeholder="Coupon code" onFocus={() => log(' coupon field focused')} />
}
function CheckoutFlag() {
const [shouldFocus, setShouldFocus] = useState(false)
return (
<>
<CouponFieldFlag shouldFocus={shouldFocus} />
<button id="apply" onClick={() => setShouldFocus(true)}>Apply coupon</button>
</>
)
}
Click “Apply coupon”, click elsewhere on the page, then click it again:
coupon field focused
The second click logged nothing:
[]
shouldFocus was already true, so setShouldFocus(true) changed nothing: no render and no effect. To make it work, you’d have to reset the flag after focusing, or bump a counter (setFocusRequest(n => n + 1)) just to make the effect run again. Both are state that exists only to fake a function call.
2. With useImperativeHandle
type CouponHandle = { focus: () => void; clear: () => void }
function CouponField({ ref }: { ref: Ref<CouponHandle> }) {
const inputRef = useRef<HTMLInputElement>(null)
useImperativeHandle(
ref,
() => ({
focus: () => inputRef.current!.focus(),
clear: () => (inputRef.current!.value = ''),
}),
[],
)
return <input id="coupon" ref={inputRef} placeholder="Coupon code" onFocus={() => log(' coupon field focused')} />
}
function Checkout() {
const couponRef = useRef<CouponHandle>(null)
return (
<>
<CouponField ref={couponRef} />
<button id="apply" onClick={() => couponRef.current!.focus()}>Apply coupon</button>
</>
)
}
Same two clicks:
coupon field focused
coupon field focused
Both clicks focused the field, with no state and no render.
3. What the parent can reach
const handle = couponRef.current as unknown as Record<string, unknown>
log('keys:', Object.keys(handle))
log('handle.value:', String(handle.value), '· handle.style:', String(handle.style))
log('is it the <input>?', handle instanceof HTMLInputElement)
keys: ["focus","clear"]
handle.value: undefined · handle.style: undefined
is it the <input>? false
The parent gets exactly focus and clear. It can’t read .value, change .style or call anything else on the input. The component can change its markup later without breaking the parent.
How it works
refas a prop. In React 19 a function component receivesreflike any other prop.useImperativeHandle(ref, () => handle)setsref.currenttohandleduring the commit (when refs are attached) and clears it on unmount.- The handle is recreated when
depschange. Methods that read state must list that state, or they’ll see old values. Methods that only use refs, like these, can use[]. - It’s still a ref.
couponRef.currentisnulluntil the child has mounted, so call it from event handlers or effects, not during render.
State or a command?
Ask: “does this change what’s shown?” If yes, it’s state or props, like isOpen, value or selectedId. If it’s “do this once, now” (focus, scroll into view, play, shake, validate and report), an imperative method is the honest model. Libraries do this too: form libraries expose setFocus(), media players seekTo(), chart wrappers resetZoom().
Common mistakes
- Exposing everything (
useImperativeHandle(ref, () => inputRef.current)). That’s the DOM node again, with no encapsulation. - Using it for things props can express. “Open the modal” is better as an
openprop if the parent needs to know whether it’s open. - Missing dependencies. A
validate()that reads state needs that state indeps. - Calling
ref.current.method()during render. Refs are set in the commit.
Interview Q&A
What does useImperativeHandle do?
It sets the parent’s ref to an object you create, instead of a DOM node. useImperativeHandle(ref, () => ({ focus() {…} }), deps) gives the parent only the methods you list.
Why not just a shouldFocus prop?
Because focusing is a command, not a state. Recorded: the second “Apply coupon” click did nothing, because setShouldFocus(true) on a value that was already true causes no render and no effect. The imperative version focused on both clicks.
How does a function component receive a ref in React 19?
As a regular ref prop. Before React 19 you had to wrap the component in forwardRef.
What can the parent do with the handle?
Only what’s in it. Recorded: Object.keys(handle) was ["focus", "clear"], handle.value and handle.style were undefined, and it wasn’t an HTMLInputElement.
When would you use it in real code?
Focusing or validating form fields, scrolling a message list to the bottom, play/pause/seek on a media wrapper, wrapping a non-React library (a map, a chart, an animation) behind a few methods.
Related
- DOM Refs and Effect Dependencies: refs to DOM nodes.
- flushSync: focus something you just rendered.
- TypeScript with React: typing props, including
Ref<…>.
Sources
- react.dev:
useImperativeHandle,refas a prop - Topic order inspired by Kent C. Dodds’ EpicReact Advanced React APIs workshop; the example app and code here are this handbook’s own.
Primary source: react.dev/reference/react/useImperativeHandle