⚛️ React Hook: useImperativeHandle
📖 Quick Summary
useImperativeHandle
lets a component customize the ref value it exposes to its parent.- It’s always used together with
forwardRef
. - Enables a parent to call specific methods on a child component instead of exposing raw DOM or implementation details.
- Syntax:tsx
useImperativeHandle(ref, () => handleObject, [deps?]);
🧠 Mental Model
- Think of
ref
as a remote control 🎮 given to the parent. - By default, the remote points to the DOM node (or class instance).
- With
useImperativeHandle
, you decide what buttons (methods/properties) appear on the remote.
🔑 Key Concepts
Works with
forwardRef
- Needed to forward the parent’s ref into the child.
Custom Handle
- Defines what the parent can access on
ref.current
.
- Defines what the parent can access on
Dependencies
- Optional dependency array ensures handle is recomputed only when needed.
Escape Hatch
- Use it only when declarative props aren’t enough (focus, scroll, animations).
💻 Code Examples
Example 1: Expose focus()
and clear()
on an Input
import { forwardRef, useImperativeHandle, useRef } from "react";
export type InputHandle = {
focus: () => void;
clear: () => void;
};
const FancyInput = forwardRef<InputHandle>(function FancyInput(_, ref) {
const inputRef = useRef<HTMLInputElement>(null);
useImperativeHandle(ref, () => ({
focus() {
inputRef.current?.focus();
},
clear() {
if (inputRef.current) inputRef.current.value = "";
},
}), []);
return <input ref={inputRef} placeholder="Type here" />;
});
export default FancyInput;
How it works (step‑by‑step):
- Parent passes a ref to
FancyInput
. - Inside child,
useImperativeHandle
maps that ref to a custom object{ focus, clear }
. - Parent can now call
ref.current.focus()
orref.current.clear()
. - Parent does not get direct access to the raw DOM element.
Example 2: Parent Using the Custom API
import { useRef } from "react";
import FancyInput, { InputHandle } from "./FancyInput";
export default function App() {
const inputRef = useRef<InputHandle>(null);
return (
<div>
<button onClick={() => inputRef.current?.focus()}>Focus</button>
<button onClick={() => inputRef.current?.clear()}>Clear</button>
<FancyInput ref={inputRef} />
</div>
);
}
How it works (step‑by‑step):
App
creates a ref of typeInputHandle
.- The ref is attached to
<FancyInput />
. - Clicking buttons calls the custom methods exposed by the child.
Example 3: Media Player Controls
import { forwardRef, useImperativeHandle, useRef, useCallback } from "react";
type PlayerHandle = {
play: () => void;
pause: () => void;
seek: (time: number) => void;
};
const VideoPlayer = forwardRef<PlayerHandle>(function VideoPlayer(_, ref) {
const videoRef = useRef<HTMLVideoElement>(null);
const play = useCallback(() => videoRef.current?.play(), []);
const pause = useCallback(() => videoRef.current?.pause(), []);
const seek = useCallback((t: number) => {
if (videoRef.current) videoRef.current.currentTime = t;
}, []);
useImperativeHandle(ref, () => ({ play, pause, seek }), [play, pause, seek]);
return <video ref={videoRef} src="/video.mp4" width="400" />;
});
export default VideoPlayer;
How it works (step‑by‑step):
- Internal
videoRef
points to the<video>
element. - Stable callbacks (
play
,pause
,seek
) are created withuseCallback
. useImperativeHandle
exposes those methods to parent.- Parent can now control video imperatively (
ref.current.play()
).
Example 4: Restrict Access (Hiding Internals)
import { forwardRef, useImperativeHandle, useRef } from "react";
type ScrollHandle = { scrollToTop: () => void };
const ScrollBox = forwardRef<ScrollHandle>(function ScrollBox(_, ref) {
const divRef = useRef<HTMLDivElement>(null);
useImperativeHandle(ref, () => ({
scrollToTop() {
divRef.current?.scrollTo({ top: 0, behavior: "smooth" });
},
}));
return (
<div ref={divRef} style={{ overflow: "auto", height: 200 }}>
{/* Large content */}
</div>
);
});
export default ScrollBox;
How it works (step‑by‑step):
- Internal
divRef
points to the scrollable container. - Instead of exposing
divRef
directly, only ascrollToTop()
method is exposed. - Parent cannot manipulate DOM directly → reduces coupling.
⚠️ Common Pitfalls & Gotchas
- ❌ Forgetting to wrap child with
forwardRef
. - ❌ Exposing entire DOM refs → defeats the purpose of hiding internals.
- ❌ Not memoizing functions → causes handle object to change each render.
- ❌ Overusing it when declarative props suffice.
✅ Best Practices
- Keep the exposed API minimal and stable.
- Use
useCallback
for stable functions. - Prefer declarative props; use imperative handle only for focus, scroll, media, or 3rd‑party APIs.
- Type the handle (
InputHandle
,PlayerHandle
) in TypeScript for safety.
❓ Interview Q&A
Q1. Why do we need useImperativeHandle
?
A: To expose a controlled, custom API to the parent via ref, instead of exposing raw DOM or implementation details.
Q2. Can useImperativeHandle
work without forwardRef
?
A: No. You need forwardRef
to pass parent’s ref into the child.
Q3. When should you avoid useImperativeHandle
?
A: When a declarative prop/state can achieve the same goal. Overusing imperative APIs increases coupling.
Q4. How do you keep the handle stable across renders?
A: Wrap functions in useCallback
and use a proper dependency array in useImperativeHandle
.
Q5. Can you expose values as well as methods?
A: Yes, but methods are safer for dynamic values (they always read the latest state).