Mastering Imperative Programming in React: A Deep Dive into useImperativeHandle
React leans declarative by default. You describe what the UI should look like and React handles the details. Sometimes that isn’t enough — you need to reach into a child component and tell it to do something specific: focus this input, scroll to that row, play this video. There’s no clean prop-shaped way to express any of that, and useImperativeHandle is the hook that fills the gap. It lets a parent grab specific methods off a child via a ref, without exposing the child’s whole internals. This guide walks through how the hook works, when to use it, and how the imperative version stacks up against the declarative one side by side.
1. Terminology Background: Imperative Programming
Before the hook itself, a quick word on what imperative programming even means.
Imperative vs. Declarative
React encourages a declarative style across the board, and useImperativeHandle is the escape hatch for the cases where that style doesn’t cut it.
- Declarative programming: focuses on what you want the code to achieve, without spelling out how.
- Imperative programming: focuses on the how, with explicit steps that produce a result.
We’ll compare both approaches with code further down.
2. Definition
useImperativeHandle lets a parent component imperatively reach into some of a child’s functions or properties. Normally, components only expose their props upward; state and methods stay private. This hook punches a hole in that wall. You pick which methods or values the parent can touch, and the parent reaches them through a ref handle.
3. Reference
useImperativeHandle(ref, createHandle, dependencies?)Parameters
ref: the ref you receive fromforwardRef, i.e. the one the parent passed down. This is the channel.createHandle: a function taking no arguments that returns the object (or value) you want to expose. Whatever it returns is what the parent sees via the ref.dependencies(optional): an array of dependencies. If any of them change the handle is rebuilt.
4. When to Use
The everyday case: you need to call a function defined inside a child component from the parent. Anything where the action is genuinely imperative (focus, scroll, play, reset) and shoehorning it into props would feel worse than the ref-based solution.
5. Example
For a parent to hand a ref to a child, the child has to forward the ref it receives. Functional components don’t accept refs by default, so you wrap them in forwardRef. The hook then customizes what that ref actually exposes.
💡 useImperativeHandle is always used in conjunction with forwardRef.
Here’s the canonical example. A parent that needs to focus an input living inside a child.
import { useImperativeHandle, forwardRef, useRef } from "react"
// child component
export const Child = forwardRef((props, ref) => {
const inputRef = useRef()
// we expose the "focusInput" method to parent
useImperativeHandle(ref, () => ({
focusInput() {
inputRef.current.focus()
},
}))
return <input ref={inputRef} />
})
// parent component
export const Parent = () => {
const inputRef = useRef()
return (
<div>
<Child ref={inputRef} />
<button onClick={() => inputRef.current.focusInput()}>Focus Input</button>
</div>
)
}
export default ParentReading from the top:
- The
Childcomponent is wrapped inforwardRefso the ref travels parent-to-child. - Inside
Child,useImperativeHandleexposes a single method,focusInput. - The
Parentcalls that method on the ref when the button is clicked.
6. Imperative vs. Declarative
Same end-result, two routes. Here they are side by side.
Imperative Example
export const ImperativeChild = forwardRef((props, ref) => {
const inputRef = useRef()
// we expose the "focusInput" method to parent
useImperativeHandle(ref, () => ({
focusInput() {
inputRef.current.focus()
},
}))
return <input ref={inputRef} />
})
export const ImperativeParent = () => {
const inputRef = useRef()
return (
<div>
<ImperativeChild ref={inputRef} />
<button onClick={() => inputRef.current.focusInput()}>Focus Input</button>
</div>
)
}- In
ImperativeChild,useImperativeHandleexposes a customfocusInput()method to the parent. - That lets
ImperativeParentcallinputRef.current.focusInput()to focus the input. - Unlike the declarative version, where React handles DOM access for you, you’re spelling out the behavior (focusing the input) through a method the child publishes.
Declarative Example
export const DeclarativeChild = forwardRef((props, ref) => {
return <input ref={ref} />
})
export const DeclarativeParent = () => {
const inputRef = useRef()
return (
<div>
<DeclarativeChild ref={inputRef} />
<button onClick={() => inputRef.current.focus()}>Focus Input</button>
</div>
)
}DeclarativeParentpasses thereftoDeclarativeChild, which forwards it onto the input itself, giving the parent direct access to the underlying DOM node.- When the button is clicked,
inputRef.current.focus()runs against that DOM node directly. - No custom methods cross the boundary. React manages the DOM element, and the parent uses the standard DOM API on it.
7. Caveats
Use useImperativeHandle sparingly. Over-reliance on imperative code makes components harder to read, harder to test and harder to refactor later. Expose only the methods or properties the parent actually needs — keep the child’s internals encapsulated, and the boundary between the two stays clean.