Forwarding a ref hands the parent the raw DOM node with every method on it, including remove() and innerHTML. useImperativeHandle(ref, createHandle, dependencies?) replaces that node with an object you define, so the parent gets exactly the operations you meant to publish and nothing else.
function MyInput({ ref, ...rest }) {
const inputRef = useRef(null);
useImperativeHandle(ref, () => ({
focus: () => inputRef.current.focus(),
clear: () => { inputRef.current.value = ''; }
}), []);
return <input ref={inputRef} {...rest} />;
}
function Form() {
const box = useRef(null); // box.current is the handle, not the <input>
return <><MyInput ref={box} placeholder="search" />
<button onClick={() => { box.current.clear(); box.current.focus(); }}>Reset</button></>;
}The third argument works like the dependency array of useEffect: with [] the handle is built once, and with dependencies listed React 7,897 rebuilds it when they change. Leaving the array out rebuilds it every render, making the handle's identity unstable for any effect that depends on it.
The methods need not mirror the DOM. A Post component can expose one scrollAndFocusAddComment() that drives two of its own children — a coarse-grained action rather than a leaked node. The handle is an API, so name it after intent. React 19 made ref an ordinary prop, so the hook no longer needs forwardRef around the component; all it ever wanted is the ref itself, whichever route delivered it.
Before reaching for it, check that the job is genuinely imperative. Scrolling, focusing, selecting text, playing media and starting an animation cannot be expressed as values. Everything else can: a dialog a parent opens with modal.current.open() should have been <Modal isOpen={open} />, because the declarative version survives a re-render, a route change and server rendering.