React 47 ⚛️ Exposing Imperative Handles with useImperativeHandle
React’s model is declarative: the parent describes what it wants, and the child renders it. The parent does not reach into the child and call the methods; it passes the props, and the child reacts. But some operations are inherently imperative. The parent needs to focus the input inside the child, scroll the list inside the child, play the video inside the child, or trigger the animation inside the child. The element is not in the parent’s JSX; it is inside the child’s template. The ref cannot reach it directly, because the ref points to the child component, not to the element inside. useImperativeHandle is the hook that lets the child expose a controlled, imperative API to the parent, without giving the parent access to the raw DOM node.
The hook is the counterpart to forwardRef. The forwardRef forwards the ref from the parent to the child, and the useImperativeHandle defines what the ref points to. Without the useImperativeHandle, the forwarded ref points to the child’s DOM node, and the parent can call any DOM method on it. With the useImperativeHandle, the ref points to the object the child constructs, and the parent can call only the methods the child exposes. The object is the imperative handle, and the methods are the public API of the child. The child decides what the parent can do, and the raw DOM node stays private.
The trade-off is the abstraction. The imperative handle is the escape hatch from the declarative model, and the escape hatch should be narrow. A child that exposes the focus, the scrollTo, and the play methods is the reasonable API. A child that exposes the raw DOM node is not, because the parent can then do anything, and the child’s internal structure is no longer encapsulated. The useImperativeHandle is the way to expose the minimum, and the minimum is the set of the operations the parent genuinely needs.
This chapter covers three areas. First, why useImperativeHandle exists — the imperative-operation problem and the encapsulation problem. Second, how to use it — the forwardRef combination, the useImperativeHandle signature, the dependency array, and the TypeScript typing. Third, the patterns and the pitfalls — the focus, the scroll, the media, and the form APIs, and the cases where the declarative approach is the better choice. The chapter ends with a complete example session, a quick reference, best practices, common pitfalls, real-world examples, and diagrams showing the imperative handle flow.
Key point: useImperativeHandle lets a child component expose a controlled imperative API to the parent through a ref. It is used with forwardRef (or with the ref-as-a-prop in React 19), and it defines what the ref points to. The ref points to the object the child constructs, not the raw DOM node, so the child controls the API. The dependency array determines when the handle is recreated, and the handle should expose the minimum the parent needs.
Why useImperativeHandle exists
The imperative-operation problem. React’s declarative model is the default: the parent passes the props, and the child renders. But some operations are not expressible as the props. The focus is the imperative command: the input is focused when the user clicks a button, and the button is the sibling, not the parent. The scrollTo is the imperative command: the list is scrolled to the bottom when the new message arrives, and the list is inside the child. The play is the imperative command: the video is played when the user presses the play button. These operations need the reference to the element, and the element is inside the child. The useImperativeHandle is the mechanism that exposes the reference.
The encapsulation problem. The forwardRef alone forwards the ref to the child’s DOM node, and the parent can call any DOM method on it. The parent can inputRef.current.remove() or inputRef.current.innerHTML = '...', which bypasses the child’s logic and breaks the encapsulation. The useImperativeHandle restricts the API: the ref points to the object the child constructs, and the parent can call only the methods the child exposes. The child decides what the parent can do, and the raw DOM node stays private.
The reuse problem. A reusable component like the FancyInput is used in many places, and the parents need to focus it. The component cannot expose the raw input, because the parents would depend on the internal structure. The useImperativeHandle exposes the focus method, and the parents call it. If the internal structure changes — the input is wrapped, the ref is moved — the focus method still works, and the parents are unaffected.
The third-party-integration problem. A component that wraps a third-party library often needs to expose the library’s imperative API. The map component wraps the Leaflet, and the parent needs to call the setView or the addMarker. The useImperativeHandle exposes the library’s methods, and the parent uses them. The child encapsulates the library, and the parent sees the controlled API.
The declarative-alternative problem. The imperative handle is the escape hatch, and the declarative approach is usually better. The focus can be done with the autoFocus prop or with the key and the useEffect. The scrollTo can be done with the scrollIntoView on the element. The play can be done with the playing prop and the useEffect. The imperative handle is for the cases where the declarative approach is awkward or impossible, not the default.
The trade-off. The imperative handle is the escape hatch from the declarative model, and the escape hatch has the cost. The parent depends on the child’s imperative API, and the API is not the part of the declarative data flow. The API can be the source of the subtle bugs: the ref is null before the mount, the handle is stale after the dependency change, the call happens at the wrong time. The trade-off is between the precision of the imperative control and the simplicity of the declarative flow.
a. The basic useImperativeHandle
The useImperativeHandle is used inside a component that receives a ref. The forwardRef wraps the component, and the useImperativeHandle defines the handle.
import { forwardRef, useImperativeHandle, useRef } from 'react';
const FancyInput = forwardRef(function FancyInput(props, ref) {
const inputRef = useRef(null);
useImperativeHandle(ref, () => ({
focus() {
inputRef.current.focus();
},
clear() {
inputRef.current.value = '';
},
}));
return <input ref={inputRef} {...props} />;
});
export default FancyInput;
The forwardRef forwards the parent’s ref to the FancyInput. The useImperativeHandle receives the forwarded ref and the factory function, and it sets the ref’s current to the object the factory returns. The object has the focus and the clear methods, and the parent can call them.
The parent uses the component with the ref:
function Parent() {
const fancyRef = useRef(null);
return (
<>
<FancyInput ref={fancyRef} />
<button onClick={() => fancyRef.current.focus()}>Focus</button>
<button onClick={() => fancyRef.current.clear()}>Clear</button>
</>
);
}
The fancyRef.current is the object with the focus and the clear methods. The parent calls them, and the child performs the imperative operation on the internal input.
The dependency array. The useImperativeHandle accepts the third argument: the dependency array. The handle is recreated when the dependencies change.
useImperativeHandle(ref, () => ({
focus() {
inputRef.current.focus();
},
}), []);
The empty array means the handle is created once, on the mount. The handle’s methods close over the inputRef, which is stable, so the handle does not need to be recreated. If the handle’s methods depend on the the props or the state, the dependencies must include them:
useImperativeHandle(ref, () => ({
focus() {
inputRef.current.focus();
if (props.selectAll) {
inputRef.current.select();
}
},
}), [props.selectAll]);
The handle is recreated when the selectAll changes, so the parent’s ref always points to the handle with the current selectAll.
The React 19 ref-as-a-prop. In React 19, the forwardRef is no longer required. The ref is a regular prop, and the component receives it directly.
function FancyInput({ ref, ...props }) {
const inputRef = useRef(null);
useImperativeHandle(ref, () => ({
focus() {
inputRef.current.focus();
},
}));
return <input ref={inputRef} {...props} />;
}
The ref is destructured from the props, and the useImperativeHandle uses it. The parent uses the component the same way:
<FancyInput ref={fancyRef} />
The React 19 approach removes the forwardRef wrapper, and the ref is the regular prop. The React 18 and the earlier versions require the forwardRef.
b. The TypeScript typing
The imperative handle has a type, and the forwardRef and the useImperativeHandle are typed accordingly.
import { forwardRef, useImperativeHandle, useRef } from 'react';
export interface FancyInputHandle {
focus: () => void;
clear: () => void;
}
const FancyInput = forwardRef<FancyInputHandle, { placeholder?: string }>(
function FancyInput(props, ref) {
const inputRef = useRef<HTMLInputElement>(null);
useImperativeHandle(ref, () => ({
focus() {
inputRef.current?.focus();
},
clear() {
if (inputRef.current) {
inputRef.current.value = '';
}
},
}));
return <input ref={inputRef} {...props} />;
}
);
export default FancyInput;
The forwardRef<FancyInputHandle, { placeholder?: string }> declares the handle type and the props type. The useImperativeHandle is typed by the FancyInputHandle, and the factory returns the matching object. The parent’s useRef<FancyInputHandle>(null) is the matching type.
The parent:
function Parent() {
const fancyRef = useRef<FancyInputHandle>(null);
return (
<>
<FancyInput ref={fancyRef} />
<button onClick={() => fancyRef.current?.focus()}>Focus</button>
</>
);
}
The fancyRef.current?.focus() uses the optional chaining, because the ref may be null before the mount.
The React 19 typing. In React 19, the ref is the regular prop, and the typing is the prop type:
interface FancyInputProps {
ref?: React.Ref<FancyInputHandle>;
placeholder?: string;
}
function FancyInput({ ref, ...props }: FancyInputProps) {
// ...
}
The ref is the optional prop of the React.Ref type, and the useImperativeHandle is typed the same way.
c. The patterns
The focus pattern. The most common use of the useImperativeHandle is the focus. The child wraps the input, and the parent focuses it.
const FancyInput = forwardRef(function FancyInput(props, ref) {
const inputRef = useRef(null);
useImperativeHandle(ref, () => ({
focus: () => inputRef.current?.focus(),
select: () => inputRef.current?.select(),
}));
return <input ref={inputRef} {...props} />;
});
The handle exposes the focus and the select, and the parent calls them.
The scroll pattern. The child wraps the list, and the parent scrolls it.
const MessageList = forwardRef(function MessageList({ messages }, ref) {
const listRef = useRef(null);
useImperativeHandle(ref, () => ({
scrollToBottom: () => {
if (listRef.current) {
listRef.current.scrollTop = listRef.current.scrollHeight;
}
},
scrollToTop: () => {
if (listRef.current) {
listRef.current.scrollTop = 0;
}
},
}));
return (
<div ref={listRef} style={{ height: 200, overflow: 'auto' }}>
{messages.map((m) => (
<p key={m.id}>{m.text}</p>
))}
</div>
);
});
The parent calls the scrollToBottom when the new message arrives:
function Chat({ messages }) {
const listRef = useRef(null);
useEffect(() => {
listRef.current?.scrollToBottom();
}, [messages]);
return <MessageList ref={listRef} messages={messages} />;
}
The media pattern. The child wraps the video, and the parent controls the playback.
const VideoPlayer = forwardRef(function VideoPlayer({ src }, ref) {
const videoRef = useRef(null);
useImperativeHandle(ref, () => ({
play: () => videoRef.current?.play(),
pause: () => videoRef.current?.pause(),
seek: (time) => {
if (videoRef.current) {
videoRef.current.currentTime = time;
}
},
}));
return <video ref={videoRef} src={src} />;
});
The parent calls the play, the pause, and the seek.
The form pattern. The child wraps the form, and the parent submits and resets it.
const Form = forwardRef(function Form({ children }, ref) {
const formRef = useRef(null);
useImperativeHandle(ref, () => ({
submit: () => formRef.current?.requestSubmit(),
reset: () => formRef.current?.reset(),
focusFirst: () => {
const first = formRef.current?.querySelector('input, select, textarea');
first?.focus();
},
}));
return <form ref={formRef}>{children}</form>;
});
The parent calls the submit, the reset, and the focusFirst.
The third-party pattern. The child wraps the third-party library, and the parent calls the library’s methods.
const Map = forwardRef(function Map({ center, zoom }, ref) {
const mapRef = useRef(null);
const mapInstanceRef = useRef(null);
useEffect(() => {
mapInstanceRef.current = new LeafletMap(mapRef.current, { center, zoom });
return () => mapInstanceRef.current?.remove();
}, []);
useImperativeHandle(ref, () => ({
setView: (center, zoom) => mapInstanceRef.current?.setView(center, zoom),
addMarker: (latlng) => mapInstanceRef.current?.addMarker(latlng),
}));
return <div ref={mapRef} style={{ height: 400 }} />;
});
The parent calls the setView and the addMarker, and the child encapsulates the Leaflet.
The declarative alternative. The imperative handle is not always the right choice. The autoFocus prop focuses the input on the mount, and the useEffect focuses it on the condition. The scrollIntoView scrolls the element, and the useEffect triggers it. The declarative approach is the simpler and the more React-idiomatic, and the imperative handle is for the cases where the declarative approach is awkward.
Complete Example Session
// ============================================
// PART 1: THE BASIC IMPERATIVE HANDLE
// ============================================
import { forwardRef, useImperativeHandle, useRef } from 'react';
const FancyInput = forwardRef(function FancyInput(props, ref) {
const inputRef = useRef(null);
useImperativeHandle(ref, () => ({
focus() {
inputRef.current.focus();
},
clear() {
inputRef.current.value = '';
},
}));
return <input ref={inputRef} {...props} />;
});
function Parent() {
const fancyRef = useRef(null);
return (
<>
<FancyInput ref={fancyRef} />
<button onClick={() => fancyRef.current.focus()}>Focus</button>
<button onClick={() => fancyRef.current.clear()}>Clear</button>
</>
);
}
// ============================================
// PART 2: THE DEPENDENCY ARRAY
// ============================================
const FancyInput = forwardRef(function FancyInput({ selectAll }, ref) {
const inputRef = useRef(null);
useImperativeHandle(ref, () => ({
focus() {
inputRef.current.focus();
if (selectAll) {
inputRef.current.select();
}
},
}), [selectAll]);
return <input ref={inputRef} />;
});
// ============================================
// PART 3: THE REACT 19 REF-AS-A-PROP
// ============================================
function FancyInput({ ref, ...props }) {
const inputRef = useRef(null);
useImperativeHandle(ref, () => ({
focus() {
inputRef.current.focus();
},
}));
return <input ref={inputRef} {...props} />;
}
// ============================================
// PART 4: THE TYPESCRIPT TYPING
// ============================================
export interface FancyInputHandle {
focus: () => void;
clear: () => void;
}
const FancyInput = forwardRef<FancyInputHandle, { placeholder?: string }>(
function FancyInput(props, ref) {
const inputRef = useRef<HTMLInputElement>(null);
useImperativeHandle(ref, () => ({
focus() {
inputRef.current?.focus();
},
clear() {
if (inputRef.current) {
inputRef.current.value = '';
}
},
}));
return <input ref={inputRef} {...props} />;
}
);
// ============================================
// PART 5: THE FOCUS PATTERN
// ============================================
const SearchInput = forwardRef(function SearchInput(props, ref) {
const inputRef = useRef(null);
useImperativeHandle(ref, () => ({
focus: () => inputRef.current?.focus(),
select: () => inputRef.current?.select(),
}));
return <input type="search" ref={inputRef} {...props} />;
});
// ============================================
// PART 6: THE SCROLL PATTERN
// ============================================
const MessageList = forwardRef(function MessageList({ messages }, ref) {
const listRef = useRef(null);
useImperativeHandle(ref, () => ({
scrollToBottom: () => {
if (listRef.current) {
listRef.current.scrollTop = listRef.current.scrollHeight;
}
},
}));
return (
<div ref={listRef} style={{ height: 200, overflow: 'auto' }}>
{messages.map((m) => (
<p key={m.id}>{m.text}</p>
))}
</div>
);
});
function Chat({ messages }) {
const listRef = useRef(null);
useEffect(() => {
listRef.current?.scrollToBottom();
}, [messages]);
return <MessageList ref={listRef} messages={messages} />;
}
// ============================================
// PART 7: THE MEDIA PATTERN
// ============================================
const VideoPlayer = forwardRef(function VideoPlayer({ src }, ref) {
const videoRef = useRef(null);
useImperativeHandle(ref, () => ({
play: () => videoRef.current?.play(),
pause: () => videoRef.current?.pause(),
seek: (time) => {
if (videoRef.current) {
videoRef.current.currentTime = time;
}
},
}));
return <video ref={videoRef} src={src} />;
});
// ============================================
// PART 8: THE FORM PATTERN
// ============================================
const Form = forwardRef(function Form({ children }, ref) {
const formRef = useRef(null);
useImperativeHandle(ref, () => ({
submit: () => formRef.current?.requestSubmit(),
reset: () => formRef.current?.reset(),
focusFirst: () => {
const first = formRef.current?.querySelector('input, select, textarea');
first?.focus();
},
}));
return <form ref={formRef}>{children}</form>;
});
// ============================================
// PART 9: THE THIRD-PARTY PATTERN
// ============================================
const Map = forwardRef(function Map({ center, zoom }, ref) {
const mapRef = useRef(null);
const mapInstanceRef = useRef(null);
useEffect(() => {
mapInstanceRef.current = new LeafletMap(mapRef.current, { center, zoom });
return () => mapInstanceRef.current?.remove();
}, []);
useImperativeHandle(ref, () => ({
setView: (center, zoom) => mapInstanceRef.current?.setView(center, zoom),
addMarker: (latlng) => mapInstanceRef.current?.addMarker(latlng),
}));
return <div ref={mapRef} style={{ height: 400 }} />;
});
// ============================================
// PART 10: THE DECLARATIVE ALTERNATIVE
// ============================================
// The imperative handle is not always the right choice.
// THE FOCUS:
// The autoFocus prop focuses on the mount.
function SearchInput({ autoFocus }) {
return <input type="search" autoFocus={autoFocus} />;
}
// THE SCROLL:
// The scrollIntoView on the element.
function Chat({ messages }) {
const endRef = useRef(null);
useEffect(() => {
endRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
return (
<div>
{messages.map((m) => (
<p key={m.id}>{m.text}</p>
))}
<div ref={endRef} />
</div>
);
}
// The declarative approach is the simpler and the more
// React-idiomatic. The imperative handle is for the
// cases where the declarative approach is awkward.
The ten parts show the basic imperative handle, the dependency array, the React 19 ref-as-a-prop, the TypeScript typing, the focus pattern, the scroll pattern, the media pattern, the form pattern, the third-party pattern, and the declarative alternative.
Quick Reference
The useImperativeHandle Signature
| Argument | Meaning |
|---|---|
ref | The forwarded ref |
createHandle | The factory returning the handle |
deps | The dependency array (optional) |
The forwardRef Combination
| Version | The approach |
|---|---|
| React 18 and earlier | forwardRef + useImperativeHandle |
| React 19 | The ref as a regular prop + useImperativeHandle |
| The handle type | interface Handle { ... } |
| The ref type | useRef<Handle>(null) |
The Handle Methods
| Method | Purpose |
|---|---|
focus() | Focus the input |
clear() | Clear the input |
select() | Select the text |
scrollToBottom() | Scroll the list |
play() / pause() | The media control |
submit() / reset() | The form control |
The Patterns
| Pattern | The handle |
|---|---|
| The focus | inputRef.current?.focus() |
| The scroll | listRef.current.scrollTop = ... |
| The media | videoRef.current?.play() |
| The form | formRef.current?.requestSubmit() |
| The third-party | The library’s methods |
The Declarative Alternatives
| The imperative | The declarative |
|---|---|
| The focus | The autoFocus prop |
| The scroll | The scrollIntoView on the element |
| The play | The playing prop + the useEffect |
| The submit | The onSubmit handler |
Best Practices
✅ Do This:
// Use the forwardRef with the useImperativeHandle
const FancyInput = forwardRef(function FancyInput(props, ref) {
useImperativeHandle(ref, () => ({ focus: () => ... })); // ✅
});
// Use the dependency array for the handle
useImperativeHandle(ref, () => ({ ... }), [selectAll]); // ✅
// Expose the minimum API
useImperativeHandle(ref, () => ({ focus, select })); // ✅
// Use the TypeScript handle interface
export interface FancyInputHandle { focus: () => void; } // ✅
// Use the optional chaining for the ref
fancyRef.current?.focus(); // ✅
// Prefer the declarative approach when it works
<input autoFocus={autoFocus} /> // ✅
❌ Don’t Do This:
// Don't expose the raw DOM node
useImperativeHandle(ref, () => inputRef.current); // the parent can do anything // ❌
// Don't forget the dependency array when the handle depends on the props
useImperativeHandle(ref, () => ({ ... })); // the stale closure // ❌
// Don't use the imperative handle for the declarative cases
useImperativeHandle(ref, () => ({ setValue: (v) => setValue(v) })); // ❌
// Don't call the ref methods before the mount
fancyRef.current.focus(); // the ref is null // ❌
// Don't forget the forwardRef in the React 18 and earlier
function FancyInput(props, ref) { ... } // the ref is undefined // ❌
// Don't overuse the imperative handle
// The declarative approach is usually better // ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
The ref is null | The call before the mount | Use the optional chaining |
| The handle is stale | The missing dependency | Add the dependency |
The forwardRef is missing | The React 18 and earlier | Add the forwardRef |
| The raw DOM is exposed | The handle returns the node | Return the object with the methods |
| The methods are not typed | The missing interface | Add the handle interface |
| The imperative handle is overused | The declarative alternative | Use the declarative approach |
| The dependency causes the recreation | The object dependency | Use the primitive or the stable |
Real-World Examples
1. The Basic Handle
const FancyInput = forwardRef(function FancyInput(props, ref) {
const inputRef = useRef(null);
useImperativeHandle(ref, () => ({ focus: () => inputRef.current.focus() }));
return <input ref={inputRef} {...props} />;
});
2. The Dependency Array
useImperativeHandle(ref, () => ({ ... }), [selectAll]);
3. The React 19 Prop
function FancyInput({ ref, ...props }) {
useImperativeHandle(ref, () => ({ ... }));
return <input {...props} />;
}
4. The Typing
export interface FancyInputHandle {
focus: () => void;
}
5. The Focus
useImperativeHandle(ref, () => ({ focus: () => inputRef.current?.focus() }));
6. The Scroll
useImperativeHandle(ref, () => ({
scrollToBottom: () => { listRef.current.scrollTop = listRef.current.scrollHeight; },
}));
7. The Media
useImperativeHandle(ref, () => ({
play: () => videoRef.current?.play(),
pause: () => videoRef.current?.pause(),
}));
8. The Form
useImperativeHandle(ref, () => ({
submit: () => formRef.current?.requestSubmit(),
reset: () => formRef.current?.reset(),
}));
9. The Third-Party
useImperativeHandle(ref, () => ({
setView: (center, zoom) => mapInstanceRef.current?.setView(center, zoom),
}));
10. The Declarative Alternative
<input autoFocus={autoFocus} />
Visual
The Imperative Handle Flow
┌──────────────────────────────────────────────────────────────┐
│ THE IMPERATIVE HANDLE FLOW │
│ │
│ The Parent: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ const fancyRef = useRef(null); │ │
│ │ │ │
│ │ <FancyInput ref={fancyRef} /> │ │
│ │ <button onClick={() => fancyRef.current.focus()}> │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ │ The ref is forwarded to the child. │
│ ▼ │
│ The Child: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ const FancyInput = forwardRef(function (props, ref) {│ │
│ │ const inputRef = useRef(null); │ │
│ │ │ │
│ │ useImperativeHandle(ref, () => ({ │ │
│ │ focus: () => inputRef.current.focus(), │ │
│ │ clear: () => inputRef.current.value = '', │ │
│ │ })); │ │
│ │ │ │
│ │ return <input ref={inputRef} />; │ │
│ │ }); │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ │ The ref points to the object with the focus and clear. │
│ ▼ │
│ The Parent calls the fancyRef.current.focus(), and the │
│ child focuses the internal input. │
│ │
└──────────────────────────────────────────────────────────────┘
The Encapsulation
┌──────────────────────────────────────────────────────────────┐
│ THE ENCAPSULATION │
│ │
│ WITHOUT the useImperativeHandle: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ const FancyInput = forwardRef((props, ref) => { │ │
│ │ return <input ref={ref} />; │ │
│ │ }); │ │
│ │ │ │
│ │ The parent's ref points to the RAW <input>. │ │
│ │ The parent can: │ │
│ │ fancyRef.current.focus() │ │
│ │ fancyRef.current.remove() ← the break │ │
│ │ fancyRef.current.innerHTML = '...' ← the break │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ WITH the useImperativeHandle: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ const FancyInput = forwardRef((props, ref) => { │ │
│ │ const inputRef = useRef(null); │ │
│ │ useImperativeHandle(ref, () => ({ │ │
│ │ focus: () => inputRef.current.focus(), │ │
│ │ clear: () => inputRef.current.value = '', │ │
│ │ })); │ │
│ │ return <input ref={inputRef} />; │ │
│ │ }); │ │
│ │ │ │
│ │ The parent's ref points to the OBJECT. │ │
│ │ The parent can: │ │
│ │ fancyRef.current.focus() │ │
│ │ fancyRef.current.clear() │ │
│ │ The parent CANNOT: │ │
│ │ fancyRef.current.remove() │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
The Dependency Array
┌──────────────────────────────────────────────────────────────┐
│ THE DEPENDENCY ARRAY │
│ │
│ const FancyInput = forwardRef(function FancyInput( │
│ { selectAll }, ref │
│ ) { │
│ const inputRef = useRef(null); │
│ │
│ useImperativeHandle(ref, () => ({ │
│ focus() { │
│ inputRef.current.focus(); │
│ if (selectAll) { │
│ inputRef.current.select(); │
│ } │
│ }, │
│ }), [selectAll]); │
│ │
│ return <input ref={inputRef} />; │
│ }); │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ The handle is recreated when the selectAll changes. │ │
│ │ The parent's ref always points to the handle with │ │
│ │ the current selectAll. │ │
│ │ │ │
│ │ Without the dependency, the handle closes over the │ │
│ │ initial selectAll, and the focus method uses the │ │
│ │ stale value. │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
The Declarative Alternative
┌──────────────────────────────────────────────────────────────┐
│ THE DECLARATIVE ALTERNATIVE │
│ │
│ THE IMPERATIVE (the useImperativeHandle): │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ const SearchInput = forwardRef((props, ref) => { │ │
│ │ const inputRef = useRef(null); │ │
│ │ useImperativeHandle(ref, () => ({ │ │
│ │ focus: () => inputRef.current.focus(), │ │
│ │ })); │ │
│ │ return <input ref={inputRef} />; │ │
│ │ }); │ │
│ │ │ │
│ │ The parent: <SearchInput ref={ref} /> │ │
│ │ ref.current.focus() │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ THE DECLARATIVE (the autoFocus): │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ function SearchInput({ autoFocus }) { │ │
│ │ return <input autoFocus={autoFocus} />; │ │
│ │ } │ │
│ │ │ │
│ │ The parent: <SearchInput autoFocus /> │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ The declarative is the simpler and the more │
│ React-idiomatic. The imperative is for the cases where │
│ the declarative is awkward. │
│ │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
useImperativeHandle | Exposes the imperative API through the ref |
| The combination | forwardRef (the React 18) or the ref prop (the React 19) |
| The factory | Returns the handle object |
| The dependency array | The recreation of the handle |
| The handle | The object with the methods |
| The ref | The parent’s reference to the handle |
| The typing | The interface Handle { ... } |
| The declarative alternative | The autoFocus, the scrollIntoView |
Key takeaways:
useImperativeHandleexposes a controlled imperative API through the ref. The child uses the hook to define what the ref points to, and the parent calls the methods. The ref points to the object the child constructs, not the raw DOM node, so the child controls the API .- The hook is used with the
forwardRef. TheforwardRefforwards the parent’s ref to the child, and theuseImperativeHandledefines the handle. In React 19, theforwardRefis no longer required, and therefis the regular prop . - The dependency array controls the recreation. The handle is recreated when the dependencies change. The empty array creates the handle once, and the dependencies create it when the props or the state change. The missing dependencies cause the stale closures .
- The handle should expose the minimum. The
focus, thescrollTo, and theplayare the reasonable API. The raw DOM node is not, because the parent can then do anything and the child’s encapsulation is broken . - The imperative handle is the escape hatch. The declarative approach is usually better: the
autoFocusfor the focus, thescrollIntoViewfor the scroll, theplayingprop for the media. The imperative handle is for the cases where the declarative approach is awkward or impossible . - The TypeScript typing has the handle interface. The
export interface FancyInputHandledeclares the methods, and theforwardRef<FancyInputHandle, Props>and theuseRef<FancyInputHandle>(null)use the type. The optional chaining handles thenullbefore the mount . - The third-party integration is the common use case. The component wraps the library and exposes the library’s methods through the handle. The parent uses the controlled API, and the child encapsulates the library’s complexity .
- The ref may be
nullbefore the mount. ThefancyRef.current?.focus()uses the optional chaining. The call before the mount is the error, and the optional chaining is the guard .
Remember: useImperativeHandle is the hook that exposes a controlled imperative API through the ref. It is used with the forwardRef (or the ref prop in React 19), and it defines what the ref points to. The child controls the API, and the raw DOM node stays private. The dependency array controls the recreation, and the handle should expose the minimum. The declarative approach is usually better, and the imperative handle is the escape hatch for the cases where the declarative is awkward. The result is the controlled imperative access, which is the foundation of the reusable component APIs.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!