useRef for Values and DOM
Mutable boxes that survive renders — DOM refs, instance variables, avoiding re-renders, and the ref-vs-state decision.
- react
- useref
- dom
- hooks
useRef returns a mutable object { current: … } that React keeps identity-stable for the lifetime of the component instance. Changing current does not trigger a re-render. That single fact drives every correct use of refs.
Two main jobs:
- Hold a DOM node (or external handle) for imperative APIs — focus, scroll, measure, play.
- Hold a mutable value across renders without becoming state — timer ids, previous values, latest callback.
Docs: useRef — react.dev.
Model
const ref = useRef(initialValue);
// ref === ref on every render
// ref.current is read/write
useState |
useRef |
|
|---|---|---|
| Change triggers re-render? | Yes | No |
| Identity of setter/box | Setter stable | Ref object stable |
| For UI that must update | Yes | No — stale UI if you only write a ref |
| For DOM / timers / “instance fields” | Awkward | Yes |
Think of refs as instance variables on function components.
DOM refs
function TextField() {
const inputRef = useRef(null);
function focus() {
inputRef.current?.focus();
}
return (
<>
<input ref={inputRef} aria-label="Search" />
<button type="button" onClick={focus}>
Focus
</button>
</>
);
}
React sets inputRef.current to the DOM node on mount and back to null on unmount (for host components). Read it in event handlers and effects — not during render for “what is on screen now” product logic, unless you accept timing subtleties.
Callback refs
When you need to know the moment a node attaches:
const [width, setWidth] = useState(0);
const measureRef = useCallback((node) => {
if (!node) return;
setWidth(node.getBoundingClientRect().width);
}, []);
return <div ref={measureRef} />;
Callback refs re-run when the callback identity changes — stabilize with useCallback if needed.
Mutable values without re-render
Timer ids
function useInterval(fn, ms) {
const fnRef = useRef(fn);
fnRef.current = fn; // always latest
useEffect(() => {
if (ms == null) return undefined;
const id = setInterval(() => fnRef.current(), ms);
return () => clearInterval(id);
}, [ms]);
}
Storing the latest fn in a ref avoids restarting the interval every render while still calling fresh logic. Pattern also appears in debounce / throttle React wrappers.
Previous value
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current; // previous, because effect runs after paint
}
Avoiding stale closures without listing deps
function Chat({ roomId }) {
const [messages, setMessages] = useState([]);
const roomIdRef = useRef(roomId);
roomIdRef.current = roomId;
useEffect(() => {
const socket = connect(roomId);
socket.on('message', (msg) => {
// can read roomIdRef.current if needed for guards
setMessages((m) => m.concat(msg));
});
return () => socket.disconnect();
}, [roomId]);
}
Prefer fixing dependency arrays correctly when possible. Refs are the escape hatch for “I need the latest X inside a long-lived subscription.”
Uncontrolled inputs
function Form({ onSave }) {
const nameRef = useRef(null);
return (
<form
onSubmit={(e) => {
e.preventDefault();
onSave({ name: nameRef.current.value });
}}
>
<input ref={nameRef} defaultValue="" name="name" />
<button type="submit">Save</button>
</form>
);
}
See Controlled vs Uncontrolled Inputs.
What not to do
// ❌ Expect UI to update
function Broken() {
const count = useRef(0);
return (
<button
type="button"
onClick={() => {
count.current += 1; // UI still shows 0
}}
>
{count.current}
</button>
);
}
If the user should see the change, use state. If only an event handler or effect cares, a ref is fine.
// ❌ Read/write ref.current during render for shared logic that should be pure
function Risky({ items }) {
const cache = useRef(new Map());
// mutating during render can break concurrent rendering assumptions
}
Prefer deriving during render without mutation, or mutate in events/effects. Concurrent React may re-run render; render must stay pure with respect to external side effects.
Forwarding refs
Child function components don’t accept ref as a normal prop unless you forward it:
const TextInput = forwardRef(function TextInput(props, ref) {
return <input ref={ref} {...props} />;
});
Or, in modern React with ref as a prop (depending on version guidance), follow your project’s React major docs. Imperative handles: useImperativeHandle for limited APIs (focus(), scrollTo()) instead of exposing the whole DOM.
Ref vs state decision tree
- Does changing it need to show up in UI? → state
- Is it a DOM node / external instance? → ref
- Is it a timer/subscription id? → ref
- Do you need previous props/state? → ref (+ effect)
- Are you trying to “fix” exhaustive-deps? → pause; maybe restructure
Footguns
- Using ref for displayed counters — no re-render.
- Reading
ref.currentin render for DOM measurement — oftennullon first pass; measure in layout effect or callback ref. - Forgetting cleanup for timers stored in refs.
- Assuming
refprop works on custom components without forwardRef / ref-as-prop support. - Storing state in refs to prevent re-renders and then wondering why children don’t update.
- Creating
useRef(expensive())—expensive()still runs every render for the argument expression; use lazy init pattern carefully (useRef(null)then assign once in effect, oruseStatelazy init for true once).
// initialValue is only used on first mount — but the expression
// useRef(compute()) still *calls* compute every render in your source
// unless you guard:
const ref = useRef(null);
if (ref.current === null) {
ref.current = compute(); // runs once; still a render-time mutation — prefer lazy useState for pure initial state
}
For expensive pure initial state, prefer useState(() => compute()). For refs, initialize to null and assign when you know the value.
Interview angle
Prompt: “What is useRef and when do you use it?”
Strong answer: “useRef gives a stable mutable box whose .current can change without re-rendering. I use it for DOM access — focus, scroll, media — and for instance-style values like timer ids or the latest callback in a subscription. If the UI must update, I use state. Refs don’t replace state management; they escape the render cycle.”
Follow-ups: forwardRef; why updating ref doesn’t re-render; difference from createRef in class components (new ref object each render if misused in function bodies without useRef).
Related on this site
- Controlled vs Uncontrolled Inputs — form refs
- useEffect Fundamentals — sync after paint
- useMemo and useCallback — other stability tools
- Rules of Hooks — call order
- Debounce Implementation — store debounced fn in a ref
- Forwarding Refs — component boundaries
Further reading
- useRef — react.dev
- Referencing Values with Refs
- Manipulating the DOM with Refs
- useImperativeHandle — react.dev
Related guides
- Custom Hooks DesignExtract reusable stateful logic with use* hooks — API design, composition, testing seams, and footguns that recreate render bugs.
- Rules of HooksWhy hooks must be top-level and only in React functions — call order, the linter, and the bugs that break when you cheat.
- useEffect FundamentalsWhen useEffect runs, how cleanup works, what belongs in the dependency array, and the bugs from treating it like componentDidMount.
- useReducer for Complex StateWhen useReducer beats useState — action tables, pure reducers, dispatch stability, context pairing, and migration patterns.
- Accessibility Patterns in ReactPractical React a11y: labels, focus management, keyboard, live regions, and composition patterns that stay accessible.