patternjavascriptreactTip
useImperativeHandle to expose a limited API through a ref
Viewed 0 times
React 16.8+
useImperativeHandleforwardRefcustom ref APIexpose methodsencapsulationimperative handle
Problem
When a parent holds a ref to a child component, it gets the raw DOM node — giving it unrestricted access to all DOM methods. Sometimes you want to expose only specific methods (like focus or scroll) without leaking the entire DOM node, or expose methods that don't exist on the DOM at all.
Solution
Combine forwardRef with useImperativeHandle to expose a custom ref object:
import { forwardRef, useImperativeHandle, useRef } from 'react';
const VideoPlayer = forwardRef(function VideoPlayer(props, ref) {
const videoRef = useRef(null);
// Expose only these methods — parent can't access videoRef.current directly
useImperativeHandle(ref, () => ({
play() {
videoRef.current.play();
},
pause() {
videoRef.current.pause();
},
seekTo(time) {
videoRef.current.currentTime = time;
},
}), []); // empty deps — stable API
return <video ref={videoRef} src={props.src} />;
});
// Parent usage
function Page() {
const playerRef = useRef(null);
return (
<>
<VideoPlayer ref={playerRef} src="/movie.mp4" />
<button onClick={() => playerRef.current.play()}>Play</button>
<button onClick={() => playerRef.current.seekTo(30)}>Skip 30s</button>
</>
);
}
import { forwardRef, useImperativeHandle, useRef } from 'react';
const VideoPlayer = forwardRef(function VideoPlayer(props, ref) {
const videoRef = useRef(null);
// Expose only these methods — parent can't access videoRef.current directly
useImperativeHandle(ref, () => ({
play() {
videoRef.current.play();
},
pause() {
videoRef.current.pause();
},
seekTo(time) {
videoRef.current.currentTime = time;
},
}), []); // empty deps — stable API
return <video ref={videoRef} src={props.src} />;
});
// Parent usage
function Page() {
const playerRef = useRef(null);
return (
<>
<VideoPlayer ref={playerRef} src="/movie.mp4" />
<button onClick={() => playerRef.current.play()}>Play</button>
<button onClick={() => playerRef.current.seekTo(30)}>Skip 30s</button>
</>
);
}
Why
useImperativeHandle lets you define the exact interface a parent can use, following the principle of least privilege. It also lets you expose non-DOM methods (like a form's validate() or a modal's animateOpen()).
Gotchas
- useImperativeHandle must be used inside a forwardRef component (or in React 19, a component that receives ref as a prop)
- Dependency array in useImperativeHandle works the same as useEffect — changes recreate the handle
- Avoid this pattern when possible — declarative state and props are preferable to imperative handles
- TypeScript: define the handle type explicitly and pass it as the first generic to forwardRef
Code Snippets
TypeScript typed useImperativeHandle
export interface DialogHandle {
open: () => void;
close: () => void;
}
const Dialog = forwardRef<DialogHandle, DialogProps>(function Dialog(props, ref) {
const [open, setOpen] = useState(false);
useImperativeHandle(ref, () => ({
open: () => setOpen(true),
close: () => setOpen(false),
}), []);
return open ? <div className="dialog">{props.children}</div> : null;
});Context
When building component libraries or wrapping media/animation elements that need an imperative API
Revisions (0)
No revisions yet.