ReactuseImperativeHandle Hook

useImperativeHandle Hook

React's data flow is intentionally one-directional: parents pass props down, children communicate back via callbacks. But some interactions are inherently imperative — "focus this input", "play this video", "scroll to this item". For these cases, React provides ref forwarding. useImperativeHandle takes this one step further: instead of exposing the raw DOM node through the ref, you can expose a custom, controlled API that hides implementation details.

The Problem: Raw Refs Expose Too Much

When you forward a ref directly to a DOM element, the parent gets access to the entire DOM node — every property and method. This leaks implementation details and gives the parent unconstrained power to manipulate your component's internals.

JSX
// Without useImperativeHandle — parent gets the raw DOM node
const Input = forwardRef(function Input(props, ref) {
  return <input ref={ref} {...props} />
})

// Parent can now do ANYTHING: ref.current.value = 'hack', ref.current.style.color = 'red'
// This breaks encapsulation — the child no longer controls its own DOM.
The Signature

JSX
useImperativeHandle(
  ref,              // the ref forwarded from forwardRef
  () => ({          // factory: return the object to expose
    methodA() { ... },
    methodB() { ... },
  }),
  deps?             // optional: deps array for when to recreate the handle
)

The object you return from the factory is what the parent gets as ref.current. Only the methods you explicitly expose are accessible — the rest of the component's internals remain hidden.

Example: A VideoPlayer Component

A media player has imperative behaviors: play, pause, seek. Rather than letting the parent reach into the video DOM node directly, we expose only the high-level methods we want to support.

TSX
import { useRef, useImperativeHandle, forwardRef } from 'react'

// Define the shape of what we expose — great for TypeScript
export type VideoPlayerHandle = {
  play(): void
  pause(): void
  seek(timeInSeconds: number): void
  getDuration(): number
}

type VideoPlayerProps = {
  src: string
  poster?: string
}

// forwardRef passes the parent's ref to us as the second argument
const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
  function VideoPlayer({ src, poster }, ref) {
    const videoRef = useRef<HTMLVideoElement>(null)

    // Expose a controlled API — NOT the raw DOM node
    useImperativeHandle(ref, () => ({
      play() {
        videoRef.current?.play()
      },
      pause() {
        videoRef.current?.pause()
      },
      seek(time: number) {
        if (videoRef.current) {
          videoRef.current.currentTime = time
        }
      },
      getDuration() {
        return videoRef.current?.duration ?? 0
      },
    }), []) // no deps needed — videoRef.current is stable

    return (
      <video
        ref={videoRef}
        src={src}
        poster={poster}
        style={{ width: '100%' }}
      />
    )
  }
)

// Parent usage
function MoviePage() {
  const playerRef = useRef<VideoPlayerHandle>(null)

  function handlePlayClick() {
    playerRef.current?.play()   // ✅ Only play/pause/seek are accessible
    // playerRef.current?.src    // ❌ TypeScript error — not part of the handle
  }

  return (
    <div>
      <VideoPlayer
        ref={playerRef}
        src="/movies/intro.mp4"
        poster="/thumbnails/intro.jpg"
      />
      <button onClick={handlePlayClick}>Play</button>
      <button onClick={() => playerRef.current?.pause()}>Pause</button>
      <button onClick={() => playerRef.current?.seek(30)}>Skip to 0:30</button>
    </div>
  )
}
Example: Focus a Custom Input

A design-system input might render a complex wrapper, but the parent should still be able to focus it programmatically. useImperativeHandle exposes just focus() and clear().

TSX
import { useRef, useImperativeHandle, forwardRef } from 'react'

type InputHandle = {
  focus(): void
  clear(): void
}

const FancyInput = forwardRef<InputHandle>(function FancyInput(props, ref) {
  const inputRef = useRef<HTMLInputElement>(null)

  useImperativeHandle(ref, () => ({
    focus() {
      inputRef.current?.focus()
      inputRef.current?.select() // also select all text when focusing
    },
    clear() {
      if (inputRef.current) {
        inputRef.current.value = ''
        inputRef.current.focus()
      }
    },
  }))

  return (
    <div className="fancy-input-wrapper">
      <input ref={inputRef} {...props} />
    </div>
  )
})

// Parent
function Form() {
  const inputRef = useRef<InputHandle>(null)

  return (
    <form onSubmit={e => { e.preventDefault(); inputRef.current?.clear() }}>
      <FancyInput ref={inputRef} placeholder="Type here..." />
      <button type="button" onClick={() => inputRef.current?.focus()}>
        Focus input
      </button>
      <button type="submit">Submit and clear</button>
    </form>
  )
}
The deps Parameter

The third argument to useImperativeHandle is an optional dependency array — the same semantics as useEffect. The handle is recreated whenever the deps change. In most cases you can omit it or pass [] because the handle methods use refs (which are stable) rather than state directly.

JSX
// If your handle methods close over props or state, list them as deps
useImperativeHandle(ref, () => ({
  submit() {
    // Uses 'onSubmit' from props — list it as a dep
    onSubmit(getCurrentValues())
  },
}), [onSubmit]) // recreate handle when onSubmit changes
useImperativeHandle Without forwardRef (React 19+)

In React 19, ref is a regular prop — you no longer need forwardRef to receive a ref from a parent. You can use useImperativeHandle directly with the ref prop.

TSX
// React 19+ — no forwardRef needed
type SliderHandle = { setValue(n: number): void }

function Slider({ ref }: { ref?: React.Ref<SliderHandle> }) {
  const [value, setValue] = useState(0)

  useImperativeHandle(ref, () => ({
    setValue(n: number) {
      setValue(Math.max(0, Math.min(100, n)))
    },
  }), [])

  return <input type="range" value={value} onChange={e => setValue(+e.target.value)} />
}
When to Use It
  • DOM focus management — focusing an input after an action

  • Media controls — play, pause, seek on audio/video elements

  • Scroll control — scrolling a list to a specific item

  • Animation triggers — starting a CSS/JS animation imperatively

  • Design-system primitives — exposing a clean API from complex component wrappers

Warning
`useImperativeHandle` is an **escape hatch**. The vast majority of component interactions should use props and callbacks. Reach for this hook only when imperative control is genuinely necessary — such as focus, scroll, and media playback — not as a substitute for well-designed props.
Note
Using `useImperativeHandle` does not prevent the parent from also passing regular props to the component. Think of the handle as a separate control channel for imperative behaviors, while props remain the primary channel for data and configuration.
Tip
Define the handle type in TypeScript (e.g. `type VideoPlayerHandle = { play(): void; pause(): void }`) and use `useRef<VideoPlayerHandle>(null)` in the parent. This gives you autocomplete and compile-time safety on the handle methods.