Reactadvanced
useImperativeHandle to expose an imperative API via ref
Published October 13, 2026
import { useRef, useImperativeHandle, type Ref } from "react";
export type VideoHandle = { play: () => void; pause: () => void; seekTo: (seconds: number) => void };
export const VideoPlayer = ({ src, ref }: { src: string; ref?: Ref<VideoHandle> }) => {
const videoRef = useRef<HTMLVideoElement>(null);
useImperativeHandle(ref, () => ({
play: () => videoRef.current?.play(),
pause: () => videoRef.current?.pause(),
seekTo: (seconds) => {
if (videoRef.current) videoRef.current.currentTime = seconds;
},
}), []);
return <video ref={videoRef} src={src} controls />;
};What
useImperativeHandle lets a component expose a small, deliberate set of methods through a ref, instead of handing the parent a raw DOM node (or nothing at all).
Why it matters
Most of the time, state and props are enough to control a child component — but some things are inherently imperative: playing a video, focusing an input, scrolling to a point. Forwarding the raw <video> element via ref works, but it hands the parent everything — pause, remove, srcObject, all of it — when you only meant to allow play/pause/seekTo. useImperativeHandle narrows that surface to exactly the methods you want to support, so the internal DOM structure of VideoPlayer can change later without breaking every caller.
How it works
- Since React 19,
refis just a regular prop — noforwardRefwrapper needed.VideoPlayeraccepts it as{ src, ref }. videoRefis the real ref to the underlying<video>element, kept private to this component.useImperativeHandle(ref, createHandle, deps)runscreateHandleand assigns its return value toref.current— that's the object the parent actually sees, not the DOM node.- The empty dependency array means the handle object is created once; each method closes over
videoRefand reads.currentfresh on every call, so it doesn't need to be recreated whensrcchanges.
Gotchas
- A parent using
ref={videoRef}on<VideoPlayer>gets the object shape defined bycreateHandle—videoRef.current.play()works, butvideoRef.current.tagNamedoesn't exist. That's the point, but it surprises people expecting a DOM node. - If a method needs a value from props (not just refs), add that value to the dependency array so the exposed function doesn't close over a stale one.
- This pattern is for genuinely imperative operations. If you're using it to sync state back up to a parent on every change, a controlled prop + callback is almost always the better fit.
Related
forwardRef— still exists and still works in React 19, but is no longer necessary just to receive aref; it remains useful for the rare case of renaming the ref parameter or wrapping a third-party component that isn't updated yet.- Controlled components (value +
onChange) — the declarative alternative; reach foruseImperativeHandleonly when the operation truly doesn't fit a prop/callback shape.