useImperativeHandle
Replaces the ref value exposed to a parent with a custom object of your own methods.
useImperativeHandle(ref, createHandle, dependencies?)- Added in React 16.8.0
Parameters
| Parameter | Type | Description |
|---|---|---|
$ref | Ref<T> | null | The ref your component received as a prop. In React 19 this is the ref prop itself; in React 18 and earlier it is the second argument your component gets from forwardRef. If it is null or undefined, the Hook does nothing. |
$createHandle | () => T | A function taking no arguments that returns the ref handle you want to expose. The return value can be anything, but is normally an object of methods. React calls it after mount and again whenever the dependencies change. |
$dependenciesoptional | unknown[] | undefined= undefined | The list of reactive values (props, state, and variables derived from them) read inside createHandle. React compares each entry with Object.is and only rebuilds the handle when one differs. Omitting the array rebuilds the handle on every render. |
Return value
undefinedNothing. Its effect is a side effect: React assigns the object returned by createHandle to the given ref's current property (or calls the ref, if it is a callback ref) instead of the DOM node React would otherwise have exposed.
Errors
It does not throw itself, but the parent gets a TypeError when it calls a method the handle does not expose — a common result of narrowing the handle and forgetting that the parent still calls the raw DOM API.
Changelog
- PHP 19.0.0Function components receive ref as a normal prop, so forwardRef is no longer required to obtain the ref argument.
- PHP 19.0.0When a ref callback returns a cleanup function, React runs that cleanup on detach rather than calling the callback again with null.
- PHP 18.0.0Strict Mode remounts components in development, so the handle is created, torn down, and created again on the initial mount.
By default a ref passed to a component that forwards it to a DOM element exposes that entire DOM node — every method and property on it. useImperativeHandle lets the child decide instead: it swaps the exposed value for an object you build, so the parent sees only the operations you meant to publish.
When to reach for it
Reach for it rarely. It is the escape hatch for behaviour that genuinely cannot be expressed as props: focusing a field, scrolling to a position, starting or stopping media playback, opening a <dialog>, or replaying an animation. It is also how a composite component exposes something coherent — a rich text editor exposing focus() and getContents(), when internally it has several DOM nodes and no single "the" element.
What trips people up
- State does not belong in a handle. If the parent needs to know something, pass a prop or lift the state. Handles are for verbs, not nouns.
- The dependency array is real. Omit it and the handle object is rebuilt every render, which reassigns the parent's
ref.currentconstantly. List whatcreateHandlecloses over, or use[]plus refs so the closure never goes stale. - The handle is assigned after commit. During the parent's first render
ref.currentis stillnull; call methods from effects or event handlers. - Narrowing is the point. Once you expose a handle, the DOM node is gone — the parent can no longer read
ref.current.valueunless you put it there.
Example
When to use it
- Expose focus() and clear() from a form field while keeping its DOM input private.
- Let a parent call play(), pause(), and seek(seconds) on a custom video player.
- Give a virtualized list a scrollToIndex(i) method the parent can call after a search.
- Publish open() and close() from a dialog wrapper that owns several nested elements.
Frequently asked questions
Do I still need forwardRef with useImperativeHandle?
No, not in React 19 and later: function components receive ref as an ordinary prop, so you destructure it and pass it straight to useImperativeHandle. forwardRef still works and is not removed, so existing wrappers keep functioning, but new code should skip it. In React 18 and earlier forwardRef is required, because ref is stripped from props and delivered as the component function's second argument.
Why is ref.current null when the parent tries to call a method?
Because React assigns the handle after the child commits, so it is still null during the parent's first render. Call the methods from an event handler or a useEffect rather than in the component body. It is also null when the child is conditionally unmounted, so guard with ref.current?.focus() anywhere the child might not be on screen.
When should I avoid useImperativeHandle?
Avoid it whenever props can express the same thing — an isOpen prop beats an open() method, because the value stays in sync with the rest of your UI automatically. Imperative handles are for one-off actions with no lasting state: focusing, scrolling, playing media, triggering an animation. If you find yourself exposing getters so the parent can read the child's state, that state should be lifted to the parent instead.
More examples
A media player with a narrow API
The parent can start, stop and seek, but cannot reach the <audio> element to change its volume, src or event handlers behind the child's back. The pane shows 'Status: idle', then 'Status: playing' or 'Status: paused' as the buttons are pressed. The empty dependency array is safe because createHandle only closes over refs and the setter, both of which are stable.
import { useImperativeHandle, useRef, useState } from "react";
function Player({ src, ref }) {
const audioRef = useRef(null);
const [status, setStatus] = useState("idle");
useImperativeHandle(ref, () => ({
play() {
audioRef.current.play();
setStatus("playing");
},
pause() {
audioRef.current.pause();
setStatus("paused");
},
seek(seconds) {
audioRef.current.currentTime = seconds;
}
}), []);
return (
<div>
<audio ref={audioRef} src={src} />
<p>Status: {status}</p>
</div>
);
}
export default function Episode() {
const playerRef = useRef(null);
return (
<div>
<Player src="/episode-01.mp3" ref={playerRef} />
<button onClick={() => playerRef.current.play()}>Play</button>{" "}
<button onClick={() => playerRef.current.pause()}>Pause</button>{" "}
<button onClick={() => playerRef.current.seek(30)}>Skip to 0:30</button>
</div>
);
}Dependencies that actually change
Because step appears in the dependency array, React rebuilds the handle when the prop changes and describe() reports 'step is 0.5' after the second button is pressed. Drop [step] to [] and the handle keeps the closure from the first render forever, so zoomIn would still add 0.25 and describe would still say 0.25 — the classic stale-closure bug this array exists to prevent.
import { useImperativeHandle, useRef, useState } from "react";
function Zoomable({ step, ref }) {
const [scale, setScale] = useState(1);
useImperativeHandle(ref, () => ({
zoomIn() {
setScale(s => s + step);
},
// Reads `step` from the closure, so it must be listed.
describe() {
return `step is ${step}`;
}
}), [step]);
return <p>Scale: {scale.toFixed(2)}</p>;
}
export default function Viewer() {
const zoomRef = useRef(null);
const [step, setStep] = useState(0.25);
const [described, setDescribed] = useState("");
return (
<div>
<Zoomable step={step} ref={zoomRef} />
<button onClick={() => zoomRef.current.zoomIn()}>Zoom in</button>{" "}
<button onClick={() => setStep(0.5)}>Use a bigger step</button>{" "}
<button onClick={() => setDescribed(zoomRef.current.describe())}>Describe</button>
<p>{described}</p>
</div>
);
}React 18 and earlier, with forwardRef
This is the same pattern written for React 18: the component is wrapped in forwardRef and receives ref as its second parameter. The behaviour is identical — the parent gets an object with focus and scrollIntoView, and reading .value on it returns undefined because the input element itself is never handed out. In React 19 you delete the wrapper and destructure ref from props.
import { forwardRef, useImperativeHandle, useRef } from "react";
// Before React 19, ref was not a prop: forwardRef supplied it as the
// second argument to the component function.
const MyInput = forwardRef(function MyInput(props, ref) {
const inputRef = useRef(null);
useImperativeHandle(ref, () => ({
focus() {
inputRef.current.focus();
},
scrollIntoView() {
inputRef.current.scrollIntoView();
}
}), []);
return <input {...props} ref={inputRef} />;
});
export default function Form() {
const inputRef = useRef(null);
function handleClick() {
inputRef.current.focus();
// inputRef.current.value would be undefined — the DOM node is not exposed.
}
return (
<form>
<MyInput placeholder="Enter your name" ref={inputRef} />
<button type="button" onClick={handleClick}>Edit</button>
</form>
);
}
Discussion