React is declarative: you set state, React updates the DOM. But some things are stubbornly imperative — focus this input, play that video, scroll here, run an animation. useImperativeHandle is the sanctioned way to do them: a child hands its parent a small remote control. I built an interactive demo where a parent drives two children entirely through refs.
▶ Live demo: https://useimperativehandle.pages.dev/
Source: https://github.com/dev48v/useimperativehandle
The pattern
// child — React 19: ref is a normal prop
function FancyInput({ ref }) {
const el = useRef(null);
useImperativeHandle(ref, () => ({
focus: () => el.current.focus(),
clear: () => { el.current.value = ""; },
shake: () => { /* toggle an animation class */ },
}), []);
return <input ref={el} />;
}
// parent — call those methods through the ref
const inputRef = useRef(null);
<FancyInput ref={inputRef} />
<button onClick={() => inputRef.current.focus()}>focus</button>
useImperativeHandle(ref, createHandle, deps) sets what ref.current becomes: not the DOM node, but the object you return. In the demo the parent calls .focus(), .clear(), .shake() on the input and .play(), .pause(), .reset() on a little player — and a log shows each call as it happens.
Why not just forward the DOM node?
You could write <input ref={ref} /> and give the parent the real element. But then the parent can do anything — read .value, mutate styles, call .remove(). There's no contract, and the moment the child's internals change, callers break.
useImperativeHandle lets the child expose exactly the verbs it chooses — three methods, nothing else. It's the difference between handing someone the keys to your house and handing them a doorbell. Capabilities, not internals.
React 19 dropped the forwardRef ceremony
Before React 19 you had to wrap the child:
const FancyInput = forwardRef((props, ref) => {
useImperativeHandle(ref, () => ({ /* ... */ }));
return <input ref={inner} />;
});
In React 19, ref is just a regular prop — accept it and pass it straight to useImperativeHandle. No forwardRef needed. The hook itself is unchanged, so the same handle works either way.
When to use it — and the honest caveat
Good fits (imperative by nature, don't map to props):
-
focus()/select()on inputs - media:
play()/pause()/seek() -
scrollIntoView(), measuring layout - triggering a one-shot animation, opening/closing a
<dialog> - resetting a complex third-party widget
But it's an escape hatch. If you find yourself calling child methods to keep state in sync, that's a smell — props, lifting state up, and context are the declarative tools that fit that job. Reach for useImperativeHandle when the action is a genuine one-time command ("do this now"), not a piece of shared state.
Click the buttons in the demo and watch the ref calls land in the log. If it clarified the imperative escape hatch, a star helps others find it: https://github.com/dev48v/useimperativehandle
Top comments (0)