10LOC
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, ref is just a regular prop — no forwardRef wrapper needed. VideoPlayer accepts it as { src, ref }.
  • videoRef is the real ref to the underlying <video> element, kept private to this component.
  • useImperativeHandle(ref, createHandle, deps) runs createHandle and assigns its return value to ref.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 videoRef and reads .current fresh on every call, so it doesn't need to be recreated when src changes.

Gotchas

  • A parent using ref={videoRef} on <VideoPlayer> gets the object shape defined by createHandlevideoRef.current.play() works, but videoRef.current.tagName doesn'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 a ref; 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 for useImperativeHandle only when the operation truly doesn't fit a prop/callback shape.