Skip to solution
hardFrontend

When and why do you use `forwardRef` with `useImperativeHandle`?

662 views
01

Understand the problem

Question presented to candidate: "You have a custom <Modal> and a parent that needs to open it. Would you reach for useImperativeHandle, and what does forwardRef have to do with it?"

What a strong answer should cover:

  • They solve two different problems that used to be paired. forwardRef passed a ref through a function component; useImperativeHandle decides what the ref points at.
  • In React 19, forwardRef is no longer neededref is a normal prop on function components. It is still exported for compatibility, but new code does not need the wrapper.
  • useImperativeHandle exists to narrow the surface. A raw DOM ref hands the parent the entire element; an imperative handle exposes only the methods you choose.
  • That matters because a ref is an escape hatch from one-way data flow, and the smaller the hatch the better — a parent that can reach innerHTML will eventually use it.
  • It is for imperative actions the DOM genuinely owns: focus, scroll, select text, play/pause media, trigger an animation.
  • It is not for state a parent should own. If the parent decides whether a modal is open, that is a prop, not a method.
  • The dependency array matters: the handle object is recreated when the deps change, so stale closures apply here too.

Clarifying questions expected:

  • "Is this genuinely imperative — focus or scroll — or is it state the parent should own?" — usually the latter, and then neither hook is right.
  • "Which React version?" — 19 removes the need for forwardRef.

Code / implementation expected: Optional. Showing the narrowed handle next to what a bare DOM ref would expose makes the case.

reactrefspatterns
02

Attempt it yourself

Sketch your approach before reading the solution — that's what interviews test.

Nudge consolestandby

Stuck? Beam a request up — the console returns a conceptual nudge that guides your logic without spoiling the implementation.

03

Study the solution

Target Audience: Engineers preparing for React interviews — assumes refs basics. Difficulty: Hard

How to read this doc: Concepts are explained in plain language first, then tagged with 📌 Interview term:. The property counts and the React 19 ref behaviour in sections 3 and 5 were **executed

Solution ready — 2 min read

Classified // press E to declassify

04

Explore the playground snippets

A narrow handle beside a bare DOM ref, with the surface counted
Run Playground
import { useRef, useState, useImperativeHandle } from "react";

// ❌ The parent receives the whole <input> element: every DOM property and
//    method, including ones that fight React (innerHTML, remove, style).
function BareInput({ ref }) {
  return <input ref={ref} defaultValue="bare" style={inputStyle} />;
}

// ✅ The parent receives exactly what is listed here. No forwardRef in sight —
//    in React 19 ref is an ordinary prop.
function NarrowInput({ ref }) {
  const inner = useRef(null);
  const [flashes, setFlashes] = useState(0);

  useImperativeHandle(ref, () => ({
    focus: () => inner.current.focus(),
    flash: () => setFlashes((n) => n + 1),
  }), []);                                   // deps: rebuild the handle when these change

  return (
    <input
      ref={inner}
      defaultValue="narrow"
      style={{ ...inputStyle, outline: flashes % 2 ? "2px solid #4f46e5" : "none" }}
    />
  );
}

const inputStyle = { width: "100%", margin: "6px 0" };

export default function App() {
  const bare = useRef(null);
  const narrow = useRef(null);
  const [log, setLog] = useState([]);
  const say = (s) => setLog((l) => [s, ...l].slice(0, 6));

  const countSurface = (obj) => {
    let n = 0;
    for (const _k in obj) n++;
    return n;
  };

  return (
    <div style={{ padding: 24, fontFamily: "system-ui", lineHeight: 1.6, maxWidth: 520 }}>
      <div style={{ border: "1px solid #ddd", borderRadius: 8, padding: 12, marginBottom: 12 }}>
        <strong style={{ fontSize: 13 }}>❌ bare DOM ref</strong>
        <BareInput ref={bare} />
        <button onClick={() => say("bare exposes " + countSurface(bare.current) + " properties")}>
          count the surface
        </button>{" "}
        <button onClick={() => { bare.current.innerHTML = ""; say("called innerHTML on it — React did not agree to that"); }}>
          reach innerHTML
        </button>
      </div>

      <div style={{ border: "1px solid #ddd", borderRadius: 8, padding: 12, marginBottom: 12 }}>
        <strong style={{ fontSize: 13 }}>✅ imperative handle</strong>
        <NarrowInput ref={narrow} />
        <button onClick={() => say("narrow exposes " + JSON.stringify(Object.keys(narrow.current)))}>
          list the surface
        </button>{" "}
        <button onClick={() => { narrow.current.focus(); say("focus() — a genuine imperative command"); }}>
          focus
        </button>{" "}
        <button onClick={() => { narrow.current.flash(); say("flash() — a method, not state the parent owns"); }}>
          flash
        </button>{" "}
        <button onClick={() => say("innerHTML reachable? " + ("innerHTML" in narrow.current))}>
          try innerHTML
        </button>
      </div>

      <pre style={{ background: "#f6f6f8", padding: 10, borderRadius: 8, fontSize: 12, minHeight: 90 }}>
{log.length ? log.join("\n") : "press the buttons"}
      </pre>

      <p style={{ fontSize: 13, color: "#666" }}>
        Count both surfaces. The narrowed one is not merely discouraged from
        touching <code>innerHTML</code> — it cannot reach it. That is the whole
        argument for <code>useImperativeHandle</code>, and it is independent of
        ref forwarding, which React 19 no longer requires.
      </p>
    </div>
  );
}
05

Join the discussion

Discussion (0)

Sign in to join the discussion.

No responses yet. Be the first to share what you think.

Transmission complete // awaiting log

KEEP THE
STREAK ALIVE.

Dossier 92 of 119 decoded in the React.js track. One more won't hurt.

Back to track