Skip to content

React · Patterns

React forwardRef & useImperativeHandle — Complete Interview Guide

7 min read Updated 2026-06-24 Share:

Practice forwardRef & useImperativeHandle interview questions

Why interviewers love this topic

forwardRef and useImperativeHandle sit at an interesting boundary in React: they are the framework's deliberate escape hatch from the "data flows down" model into the imperative world. Senior React interviewers ask about them because the answers reveal whether a candidate truly understands React's data model, or is just comfortable with the happy path.

The questions escalate fast. "What is forwardRef?" is the warm-up. The real test is "When wouldn't you use an imperative handle?" or "Show me how you'd type this in TypeScript." This guide builds up the full mental model so you can answer from understanding, not memorisation.

Why function components can't accept refs by default

Every React ref points to something. For a DOM element like <input>, the ref points to the underlying HTMLInputElement. For a class component, it points to the component instance. Function components have neither — they are plain functions that return JSX and leave no persistent object behind.

This is intentional. React's design pushes you toward props for all parent–child communication. Refs to children are the escape hatch, and React makes you opt in explicitly.

React.forwardRef — the opt-in mechanism

forwardRef wraps a render function and injects the parent-supplied ref as a second argument, alongside props. The result is a component that looks and renders like any other but correctly threads the ref through to whatever you attach it to internally.

import { forwardRef } from 'react'

const FancyInput = forwardRef(function FancyInput(props, ref) {
  // attach the forwarded ref to the actual DOM input
  return <input ref={ref} className="fancy" {...props} />
})

function Form() {
  const inputRef = useRef(null)
  return (
    <>
      <FancyInput ref={inputRef} placeholder="Name" />
      <button onClick={() => inputRef.current.focus()}>Focus</button>
    </>
  )
}

Without forwardRef, the ref attribute on <FancyInput> would silently be dropped and inputRef.current would remain null.

Always use a named function expression inside forwardRef. React DevTools uses the function name as the display name — anonymous wrappers show up as ForwardRef, which makes debugging miserable.

useImperativeHandle — curating the public API

Forwarding the raw DOM ref works fine for thin wrappers, but it gives the parent unrestricted access to the node. Any method that exists on HTMLInputElement is now accessible — including ones you never intended to expose.

useImperativeHandle(ref, createHandle, deps?) solves this by replacing what the parent sees at ref.current with a custom object you define:

const VideoPlayer = forwardRef(function VideoPlayer({ src }, ref) {
  const videoRef = useRef(null)   // private — never reaches the parent

  useImperativeHandle(ref, () => ({
    play:  () => videoRef.current.play(),
    pause: () => videoRef.current.pause(),
    seek:  (t) => { videoRef.current.currentTime = t },
  }), [])  // empty deps — handle is stable

  return <video ref={videoRef} src={src} />
})

The parent can now call player.current.play() but cannot touch videoRef.current directly. The <video> element can be swapped for any other implementation without breaking the parent.

The full pattern combined

The canonical recipe has four parts: keep an internal DOM ref private, wire it up to the DOM element, expose only selected methods via useImperativeHandle, and wrap the whole thing in forwardRef.

const SmartInput = forwardRef(function SmartInput(props, ref) {
  const inputRef = useRef(null)            // 1. private ref

  useImperativeHandle(ref, () => ({        // 2. curated handle
    focus: () => inputRef.current.focus(),
    blur:  () => inputRef.current.blur(),
    clear: () => { inputRef.current.value = '' },
  }), [])

  return <input ref={inputRef} {...props} />  // 3. wire private ref
})

This is the answer interviewers are hoping to hear when they ask "show me forwardRef and useImperativeHandle working together."

When to use an imperative handle — and when not to

The golden rule: use an imperative handle for actions, not for data.

Appropriate (action)Avoid (data)
input.focus()list.setItems(data) — use a prop
player.play()modal.setTitle(t) — use a prop
dialog.open()form.getValues() — use a callback/state
list.scrollTo(3)chart.updateColor(c) — use a prop

If the parent needs to change what renders, use a prop. If it needs to trigger an event, an imperative handle is appropriate.

TypeScript typing

The type parameters for forwardRef are <HandleType, PropsType> — handle first, props second. Always export the handle interface so consumers can type their useRef calls.

export interface ModalHandle {
  open(): void
  close(): void
}

interface ModalProps {
  title: string
  children: React.ReactNode
}

const Modal = forwardRef<ModalHandle, ModalProps>(function Modal(
  { title, children },
  ref
) {
  const [visible, setVisible] = React.useState(false)

  useImperativeHandle(ref, () => ({
    open:  () => setVisible(true),
    close: () => setVisible(false),
  }))

  if (!visible) return null
  return <dialog open><h2>{title}</h2>{children}</dialog>
})

// Consumer
const modalRef = React.useRef<ModalHandle>(null)
modalRef.current?.open()   // fully typed

Inside the render function, the parameter type for the forwarded ref is ForwardedRef<T> (a union of callback ref, object ref, and null), not Ref<T>.

Composing with React.memo

Both memo and forwardRef are higher-order wrappers. Apply forwardRef first (innermost), then memo:

const MemoInput = memo(forwardRef(function MemoInput(props, ref) {
  return <input ref={ref} {...props} />
}))

If you accidentally apply memo first and then forwardRef, it still works in practice because React resolves the wrapper chain, but the conventional order makes the intent clear.

The deps array matters

useImperativeHandle's third argument works exactly like useEffect's dependency array.

  • [] — create the handle once. Safe when the methods only call setters or refs (which are stable across renders).
  • [count] — recreate the handle when count changes. Necessary when a method closes over a state value and you need it to be current.
  • omitted — recreate on every render. Wasteful but correct.
useImperativeHandle(ref, () => ({
  getCount: () => count,   // closes over count — needs count in deps
}), [count])

React 19 changes

React 19 promotes ref to a first-class prop. Function components can receive it directly without forwardRef:

// React 19 — no forwardRef needed
function FancyInput({ ref, ...props }: React.ComponentProps<'input'>) {
  return <input ref={ref} className="fancy" {...props} />
}

forwardRef still works in React 19 (it's not removed, just legacy). useImperativeHandle is unchanged and continues to work with the ref prop directly. For codebases targeting React 18 and earlier, forwardRef remains mandatory.

Testing imperative handles

React Testing Library works well here. Create a ref, render the component with it, then call the handle method and assert the observable result:

import { createRef } from 'react'
import { render, screen } from '@testing-library/react'
import SmartInput from './SmartInput'

test('clear() empties the input', () => {
  const ref = createRef()
  render(<SmartInput ref={ref} defaultValue="hello" data-testid="inp" />)
  ref.current.clear()
  expect(screen.getByTestId('inp').value).toBe('')
})

Test what the handle does (observable DOM behaviour), not how it is implemented.

Key Takeaways

  • Function components have no instance, so they can't accept a ref without explicitly opting in via forwardRef (React ≤ 18) or a ref prop (React 19+).
  • forwardRef(fn) passes the parent's ref as the second argument to fn, letting you attach it to any DOM node or child component inside.
  • useImperativeHandle(ref, createHandle, deps) replaces ref.current with a custom object — use it to expose a curated, stable API instead of the raw DOM node.
  • Prefer imperative handles for actions (focus, play, scroll, open); prefer props for data changes.
  • TypeScript: forwardRef<HandleType, PropsType> — handle type first, props second. Export the handle interface so consumers can type their refs.
  • Compose with memo as memo(forwardRef(...)).
  • React 19 makes ref a regular prop, deprecating forwardRef for new code.
  • Test handles by asserting observable DOM or state changes, not internal implementation.

More ways to practice

The self-quiz is live. Get notified when mock interviews and new question packs drop.

or
Join our WhatsApp Channel