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);Output (verified)
render, clicks seen: 0 ref callback: P input: Search render, clicks seen: 2 ref cleanup ref callback: P input after hiding: null
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
- First render: clicks.current is 0. React attaches the input to input.current after commit.
- The code adds 2 to clicks.current. No render happens: a ref is a plain box React does not watch.
- 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.
- 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?
Changing a ref does not schedule a render, so the screen shows whatever value was there at the last render. Values that appear on screen belong in state. It is tempting to use a ref as “state without re-renders”.
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.”
Follow-up What is useRef used for besides DOM access?
Follow-up Why not read refs during render?
Follow-up When would you use useImperativeHandle?
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.
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
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
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
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.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
useRefused for besides DOM access?