Back to React
React
medium
mid

Why do we need the useRef hook in React?

`useRef` stores a mutable value that persists across renders without triggering re-renders. Two main uses: (1) hold a reference to a DOM node (`ref={ref}`) for measurement/focus/imperative ops, (2) store any mutable value (timer ids, latest values, previous props) that shouldn't cause re-renders. `.current` is the only field; updating it is intentional, not a state change.

3 min read·~8 min to think through

Two distinct uses

1. DOM refs

tsx
function Input() {
  const inputRef = useRef<HTMLInputElement>(null);
  useEffect(() => { inputRef.current?.focus(); }, []);
  return <input ref={inputRef} />;
}

ref={inputRef} assigns the DOM node to inputRef.current after mount.

2. Mutable values that don't cause re-renders

tsx
function Stopwatch() {
  const [time, setTime] = useState(0);
  const intervalRef = useRef<number | null>(null);

  useEffect(() => {
    intervalRef.current = window.setInterval(() => setTime((t) => t + 1), 1000);
    return () => clearInterval(intervalRef.current!);
  }, []);

  return <p>{time}</p>;
}

The interval id needs to survive between renders but mustn't cause re-renders when stored.

vs useState

useRefuseState
Triggers re-renderNoYes
Value preserved across rendersYesYes
Updateref.current = ...setX(...)

If reading the value should NOT cause UI updates, use useRef. If the UI must reflect the value, use useState.

Common patterns

Previous value

tsx
function usePrevious<T>(value: T): T | undefined {
  const ref = useRef<T>();
  useEffect(() => { ref.current = value; }, [value]);
  return ref.current;
}

Latest value (escape stale closure)

tsx
function useLatest<T>(value: T) {
  const ref = useRef(value);
  useEffect(() => { ref.current = value; });
  return ref;
}

function Search({ query, onSearch }) {
  const onSearchLatest = useLatest(onSearch);
  useEffect(() => {
    const id = setInterval(() => onSearchLatest.current(query), 1000);
    return () => clearInterval(id);
  }, []);   // safe — reads latest via ref
}

Avoids stale-closure bugs without putting the callback in deps.

Caching expensive value

tsx
function Lazy() {
  const expensiveRef = useRef<Result | null>(null);
  if (expensiveRef.current === null) {
    expensiveRef.current = computeExpensive();
  }
  return <Result data={expensiveRef.current} />;
}

Initialize once; avoid recomputing on every render. (React 19 has use for this; useRef pattern works in 18.)

Imperative handle to a child

See [[what-does-forwardref-do-and-when-would-you-reach-for-useimperativehandle]].

Anti-patterns

  • Using useRef when state is what you need (UI should update — use useState).
  • Storing render-affecting data in a ref (UI won't update).
  • Mutating ref.current inside render — should be in effect or event handler.
  • Using ref as poor-man's state management.

Strict Mode

useRef is stable across the dev double-mount; .current is preserved. But effects re-run, so don't put one-time side effects in effects without idempotent design.

Interview framing

"useRef is for mutable values that persist across renders without triggering re-renders. Two main uses: a ref to a DOM node for imperative ops (focus, measure, scroll), and a mutable container for things like interval ids, previous values, latest-value escape hatches for stale closures, and one-time initialization. The contrast with useState: useState triggers re-renders when set; useRef does not. So if updating shouldn't update the UI, useRef; if it should, useState. Don't mutate refs inside render — do it in effects or event handlers."

Follow-up questions

  • Why doesn't useRef trigger re-renders?
  • Show me the latest-ref pattern for callbacks.
  • When would useState be wrong and useRef right?

Common mistakes

  • useRef when state is needed.
  • Mutating ref.current in render.
  • Using ref as state-management substitute.

Performance considerations

  • Cheaper than state for non-rendering values. No re-render cost.

Edge cases

  • Refs in Strict Mode (preserved).
  • Refs in SSR (null until hydration).
  • Lazy initialization patterns.

Real-world examples

  • DOM focus, measure, scroll, intervals, latest-value refs, prev-value comparisons.

Senior engineer discussion

Seniors distinguish UI-relevant state from internal bookkeeping and reach for useRef intentionally.

Related questions