Engrain by ByteRush

RCT.03.03 Mid-level· 10 minCore — asked often

useRef and DOM refs: values that persist without re-rendering

Refs as mutable boxes that do not trigger renders, DOM refs, ref as a plain prop in React 19, ref callbacks with cleanup, useImperativeHandle, and why you should not read or write refs during render.

Comes up in: React, Coding

Learn#

Karan stores a timer id in a useState, and every setTimer causes a render the user never sees. Elsewhere, he stores a click count in a plain variable, and it resets to 0 on every render.

A ref is a box that keeps its value between renders, like state, but changing it does not trigger a render. useRef(initial) returns { current: initial }, the same object on every render. Use it for timer ids, the previous value of a prop, or a DOM node.

Pass a ref to an element’s ref prop, and React puts the DOM node in ref.current after commit:

import { GlobalRegistrator } from "@happy-dom/global-registrator";
GlobalRegistrator.register(); // a DOM for Node; in a browser you already have one
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
const { createElement: h, useRef, useState, act } = await import("react");
const { createRoot } = await import("react-dom/client");

function SearchBox({ ref }) { // React 19: ref is a plain prop, no forwardRef
  return h("input", { ref, placeholder: "Search" });
}
let page;
function Page() {
  const clicks = useRef(0); // a mutable box: changing it does not re-render
  const input = useRef(null);
  const [shown, setShown] = useState(true);
  page = { clicks, input, hide: () => setShown(false) };
  console.log("render, clicks seen:", clicks.current); // reading a ref in render: demo only
  return h("div", null,
    shown && h(SearchBox, { ref: input }),
    h("p", { ref: (node) => { console.log("ref callback:", node.tagName); return () => console.log("ref cleanup"); } }, "hi"));
}
const root = createRoot(document.createElement("div"));
await act(() => root.render(h(Page)));
page.clicks.current += 2;
console.log("input:", page.input.current.placeholder);
await act(() => page.hide());
console.log("input after hiding:", page.input.current);
This example uses Node.js, so it runs in our checks rather than in your browser.

Output (verified)

render, clicks seen: 0
ref callback: P
input: Search
render, clicks seen: 2
ref cleanup
ref callback: P
input after hiding: null
Refs change without renders; DOM refs follow the DOMStep 1 of 4

State

  • clicks.current = 0
  • input.current = <input>

Console

    empty

    First render: clicks.current is 0. React attaches the input to input.current after commit.

    Read the steps as text
    1. First render: clicks.current is 0. React attaches the input to input.current after commit.
    2. The code adds 2 to clicks.current. No render happens: a ref is a plain box React does not watch.
    3. A state change re-renders Page. The inline ref callback is a new function, so React cleans up the old one and calls the new one.
    4. The search box was hidden, so React set input.current back to null.

    Predict#

    Predict first

    You show ref.current.count in the JSX and increase it in a click handler. What does the screen show?

    Interview angle#

    Say it like this — about 30 seconds

    “useRef gives a mutable box whose current value persists across renders but does not trigger one when it changes. Besides DOM access, I use it for timer and subscription ids, previous values, and the latest callback for an event listener. I avoid reading or writing refs during render, because the result would not be pure. In React 19, ref is a normal prop on function components, so forwardRef is not needed, and ref callbacks can return a cleanup. useImperativeHandle exposes a small custom API instead of the whole DOM node.”

    Stays on your device. Nothing is uploaded.
    Follow-up What is useRef used for besides DOM access?
    Timer and interval ids, AbortControllers, previous values, flags such as “is mounted”, and the latest callback for long-lived listeners.
    Follow-up Why not read refs during render?
    The value may change without a render, so the output would depend on something React does not track, which breaks purity.
    Follow-up When would you use useImperativeHandle?
    To expose a small API such as focus() or scrollToItem(i) from a component, instead of handing out its DOM node.

    Practice#

    Guided

    Write mergeRefs, a utility many component libraries need, with React 19 cleanup support.

    Done when

    • Object refs get the node; function refs are called with it; null refs are ignored.
    • The returned cleanup runs each function ref’s cleanup, or calls it with null.
    • The cleanup sets object refs back to null.
    Your solution
    Ctrl/⌘ + Enter

    Remember#

    Try to answer each card before you open it. Then add them to your reviews.

    Does changing ref.current cause a re-render?

    No.

    When does React set a DOM ref's current?

    After commit; it is set back to null when the element is removed.

    Do you still need forwardRef in React 19?

    No. ref is a regular prop on function components.

    What can a React 19 ref callback return?

    A cleanup function, called when the element is removed or the callback changes.

    Go deeper

    Four layers. Stop at the one your interview needs; each ends with a quick check.

    The core ideaEvery round
    Read and write refs in event handlers and effects, not during render. The only exception is lazy setup: if (ref.current === null) ref.current = new Thing().
    Check yourself: Where should you read ref.current?

    In event handlers and effects, not during render.

    How it works underneathSenior rounds
    A ref callback is called with the node after commit. In React 19 it may return a cleanup. An inline callback is a new function each render, so React cleans up and calls it again, as shown above; wrap it in useCallback if that matters Source: React: ref callback function.
    Check yourself: Why does an inline ref callback run on every render?

    It is a new function each render, so React cleans up the old and calls the new.

    Edge cases and trapsWhere people slip
    In React 19, function components get ref as a normal prop, so forwardRef is no longer needed; it still works Source: React 19 release post.
    Check yourself: What changed about ref in React 19?

    Function components receive ref as a normal prop; forwardRef is no longer needed.

    In productionStaff-level answers

    useImperativeHandle(ref, () => ({ focus, clear })) exposes an imperative handle, a small chosen API, instead of the raw node.
    Prefer props and state; use imperative APIs for focus, scrolling and media playback.

    Check yourself: What does useImperativeHandle return to the parent?

    A custom object you define, instead of the DOM node.

    Finished the five steps? Mark it, and its cards join your reviews.

    Last checked 2026-11-01. Spot a mistake? Use “Report an error” below — fixes are listed on the corrections page.

    Interview questions this answers

    • What is useRef used for besides DOM access?