Topic 8.4
useImperativeHandle: Exposing a Small API
In one line
useImperativeHandle(ref, () => ({ ...methods }), deps) makes a parent's ref point at an object you choose instead of a DOM node, so you expose a few safe methods such as focus, scrollToBottom or reset and keep the rest of your component private.
Think of it like this
A TV remote. The TV has hundreds of parts inside, but the remote gives you only a few buttons: power, volume, channel. You can't reach in and rewire the screen. useImperativeHandle builds a remote control for your component: the parent gets a small object with a few buttons (methods) and nothing else.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- useImperativeHandle
- A hook that sets what a parent's ref points to: an object you build, not a DOM node.
- Handle
- The object the parent receives in
ref.current, usually with a few methods. - Imperative
- Giving step-by-step commands, like
video.play(). The opposite of describing a result with props. - createHandle
- The function you pass to the hook. It returns the handle object.
- Internal ref
- A ref the component keeps for itself, pointing at its own DOM nodes, never given to the parent.
- API
- Application programming interface: the list of things other code is allowed to call on your code.
Step by step
01Start from a forwarded ref
This component hands the parent its whole <input>. The parent could do anything with it, even ref.current.remove().
function SearchBox({ ref }) {
return <input ref={ref} type="search" />;
}02Keep the DOM node to yourself
Give the input your own internal ref. The ref prop from the parent is no longer attached to any element.
function SearchBox({ ref }) {
const inputRef = useRef(null); // private
return <input ref={inputRef} type="search" />;
}03Expose only what the parent needs
Call useImperativeHandle with the parent's ref and a function that returns a small object. The methods use the internal ref. Pass [] as dependencies, because the methods only use the stable ref.
function SearchBox({ ref }) {
const inputRef = useRef(null);
useImperativeHandle(ref, () => ({
focus() {
inputRef.current.focus();
},
clear() {
inputRef.current.value = "";
},
}), []);
return <input ref={inputRef} type="search" />;
}04Call the methods from the parent
The parent uses searchRef.current.focus() and searchRef.current.clear() in its handlers. searchRef.current.style is undefined: the DOM node isn't exposed.
05Ask: could this be a prop?
Before adding a method, check whether a prop would do. Opening, closing, selecting and filtering are state the parent can pass down. Focus, scroll and media playback are DOM actions with no prop equivalent, which is where handles belong.
Try it yourself
- 1
Add a method
In the first example, add
select()to the handle (it callsinputRef.current.select()) and a Select all button inAppthat uses it. - 2
Drop the dependency array
Remove the
[]fromuseImperativeHandlein the chat log and add aconsole.log("handle created")insidecreateHandle. Add a state counter toAppthat re-renders it. How often is the handle created now? (After every render ofChatLog.) - 3
Turn a method into a prop
Imagine
HelpPanelhadopen()andclose()methods. Write down what the parent's JSX would look like with them, then compare with the prop version. Which one shows you, just by reading, when the panel is visible?
Code & diagrams
The parent's ref holds `{ focus, clear }`, not the input. It can clear and focus the box, but it can't touch styles or remove the node. (The second small root at the bottom exists only to peek at the handle.)
Preview
Console
Nothing logged yet.Expected console output
value now: "" | focused: true
handle keys: [ 'focus', 'clear' ] | style: undefinedScrolling is a DOM action with no prop, so a small handle is a good fit. The messages themselves still arrive as a prop.
Preview
Console
Nothing logged yet.Expected console output
at the bottom: true
at the top: trueOpening is just data: `isOpen`. Anyone reading `App` sees when the panel shows, DevTools shows the state, and the panel can't get out of sync with the parent. An `open()` method on a handle would hide all that.
Preview
Console
Nothing logged yet.Expected console output
HelpPanel rendered, isOpen = false
HelpPanel rendered, isOpen = trueBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Call a method that isn't exposed
In the first example, change the Clear button to call searchRef.current.select().
Break #2
Use a value from an old render inside the handle
Add const [count, setCount] = useState(0) to a component and expose getCount() { return count; } with the dependency array []. Increase the count, then call getCount() from the parent.
Myth vs fact
Myth
useImperativeHandle is how a parent controls a child.
Fact
Props are how a parent controls a child. A handle is for the few DOM-like commands props can't express.
Myth
The handle replaces forwarding the ref.
Fact
You still receive the ref (as a prop in React 19, or from forwardRef) and pass it to the hook. The hook decides what the ref points to.
Myth
The handle is created during render.
Fact
React calls createHandle during the commit, when refs are attached, and sets the ref to null when the component unmounts.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Internally
useImperativeHandleis a layout effect that setsref.current = createHandle()(or calls a callback ref with it) and clears it on cleanup. That's why it follows the dependency rules of effects and why the exhaustive-deps lint rule checks it. - ▸
Handles can pass through several layers: a
DatePickercan exposefocus()that calls the handle of an innerTextField, which calls the DOM. Each layer keeps its own private details. - ▸
In TypeScript, describe the handle with an interface and type the ref with it:
useRef<SearchBoxHandle>(null)in the parent andref?: React.Ref<SearchBoxHandle>in the child's props (Phase 12). - ▸
Libraries such as rich text editors, video players and virtualised lists commonly expose handles (
editor.focus(),list.scrollToIndex(500)), because those actions really are commands. Data still flows through props.
Remember this
- 1
In Topic 8.3 a component forwarded its
refto a DOM element, so the parent got the whole node: it could focus it, but also change its styles, remove its children or read its value. Sometimes that is too much power.useImperativeHandlelets you decide exactly what the parent'sref.currentwill be. - 2
Imperative means "giving a command":
player.play(),input.focus(). React prefers declarative props (<Player playing={true} />). The word in the hook's name is a hint: you are adding commands to a component, which should be the exception. - 3
The signature is
useImperativeHandle(ref, createHandle, dependencies?).refis the ref your component received as a prop (React 19) or fromforwardRef.createHandleis a function returning the object you want to expose, usually methods that use your own internal refs.dependenciesworks like an effect's: React re-creates the handle when one changes; leave it out and the handle is rebuilt after every render. - 4
When React sets it: during the commit, at the same time refs are attached (before layout effects), React calls
createHandleand puts the result inref.current. On unmount it setsref.currentback tonull. So, like any ref, read it in event handlers and effects, not during render. - 5
Good uses: methods that match real DOM actions you can't express as props:
focus(),blur(),scrollIntoView(),scrollToBottom(),select(), playing and pausing media, or resetting an uncontrolled form. Each method is small and calls a method on an internal DOM ref. - 6
When not to use it: if something can be a prop, make it a prop. Instead of
modalRef.current.open(), write<Modal isOpen={isOpen} />. Instead oflistRef.current.setItems(...), passitems. Props keep the data flow one-way and visible: anyone reading the parent's JSX sees what the child shows. Imperative handles hide state changes inside method calls that React and DevTools don't track.
Explain it without notes
What does useImperativeHandle do and why would you use it instead of forwarding a ref to a DOM node?
Explain each argument of useImperativeHandle(ref, createHandle, deps).
When should you not use useImperativeHandle? Give an example and the better alternative.
Practice
Write a VideoBox component that exposes only play() and pause() to its parent, using an internal ref on a <video> element.
Write an uncontrolled NoteForm (inputs with defaultValue) that exposes reset(), which empties every field and focuses the first one.
Trade-offs
- ↔
A handle keeps your DOM private and your API small, but method calls are invisible in the JSX and in DevTools. Use it for commands, keep data in props.
- ↔
Handles with many methods often mean state is in the wrong component. If the parent calls
setX,setY,getZon a child, lift that state up instead (Topic 6.1).
Done when you can
I can expose a small handle with
useImperativeHandleusing an internal ref.I can explain what the parent's
ref.currentholds when a handle is used.I can choose the right dependency array for a handle.
I can tell when a prop is better than an imperative method.
I know React sets the handle during the commit and clears it on unmount.