Skip to the solution
HardFrontend6 min read

When and why do you use forwardRef with useImperativeHandle?

662 views

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 needed β€” ref 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.

  • react
  • refs
  • patterns

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

6 min read Β· 9 sections

Try it in the playground

Open any example in the playground to run and change it.

ReactA narrow handle beside a bare DOM ref, with the surface counted
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>
  );
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 92 of 119 in React.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All React.js questions
Up nextHard

What underlying mechanism makes the Rules of Hooks mandatory?

Continue