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.
// 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
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.
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().
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.
// 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 changesuseImperativeHandle 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.
// 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