$ emrebener
home topics react mastering imperative programming in react: a deep dive into useimperativehandle

Mastering Imperative Programming in React: A Deep Dive into useImperativeHandle

author: emre bener read time: 3 min about: useimperativehandle, imperative programming, react
published: updated: mentions: declarative programming, forwardref, javascript

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 from forwardRef, 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 Parent

Reading from the top:

  • The Child component is wrapped in forwardRef so the ref travels parent-to-child.
  • Inside Child, useImperativeHandle exposes a single method, focusInput.
  • The Parent calls 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, useImperativeHandle exposes a custom focusInput() method to the parent.
  • That lets ImperativeParent call inputRef.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>  
  )  
}
  • DeclarativeParent passes the ref to DeclarativeChild, 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.