Back to React
React
medium
mid

When should you use refs instead of state in React?

Refs for values that need to persist across renders but **don't drive rendering**: DOM nodes, timer/interval ids, mutable counters, latest-value mirrors for stale-closure fixes, third-party instances. State for values that, when changed, must trigger a re-render. Mutating a ref doesn't re-render; setting state does. Don't read/write refs during render — only in effects and handlers.

4 min read·~10 min to think through

Refs and state are both per-component memory across renders. The key difference: state changes trigger re-renders; ref mutations don't. Pick by whether the value should drive rendering.

Use refs when

1. Accessing DOM nodes

jsx
const inputRef = useRef(null);
const focus = () => inputRef.current?.focus();
return <input ref={inputRef} />;

React doesn't expose DOM nodes any other way.

2. Storing timer / interval / observer ids

jsx
const timerRef = useRef(null);
useEffect(() => {
  timerRef.current = setInterval(...);
  return () => clearInterval(timerRef.current);
}, []);

The id doesn't change rendering — just cleanup.

3. Mutable values that don't drive UI

jsx
const renderCount = useRef(0);
useEffect(() => { renderCount.current++; });

You want the value across renders but you're not displaying it (or you display it via state separately).

4. Latest-value mirror for stale closures

jsx
const latestValue = useRef(value);
useEffect(() => { latestValue.current = value; });

const callback = useCallback(() => {
  // always reads latest, even with empty deps
  use(latestValue.current);
}, []);

See [[why-does-setcountcount-1-inside-usecallback-capture-stale-state]] for the bug this fixes.

5. Third-party library instances

jsx
const mapRef = useRef(null);
useEffect(() => {
  mapRef.current = new Mapbox.Map({...});
  return () => mapRef.current.destroy();
}, []);

The map instance lives outside React's tree but tied to component lifecycle.

6. Tracking previous value

jsx
function usePrevious(value) {
  const ref = useRef();
  useEffect(() => { ref.current = value; });
  return ref.current;
}

7. Imperative animations / direct DOM writes

Per-frame updates via rAF — write to element.style.transform via refs, don't put position in state. See [[build-a-cursor-tracker]].

Use state when

The value, when changed, should cause the UI to update:

  • Input value displayed in the UI.
  • Toggle (modal open/closed).
  • Fetched data being rendered.
  • Anything visible.

The rule

If changing the value should re-render the component or its children, it's state. If the value is bookkeeping that doesn't drive what users see, it's a ref.

Don't do this

Mutating a ref to "trigger" a re-render

jsx
// BAD
const ref = useRef(0);
const inc = () => ref.current++;   // UI never updates

Refs don't re-render. Use state.

Reading/writing refs during render

jsx
function Bad() {
  const ref = useRef(0);
  ref.current++;             // BAD — side effect during render
  return <p>{ref.current}</p>;
}

React's render must be pure. Read/write refs in effects or handlers only. (Exception: lazy ref initialization on first render is okay.)

Putting DOM nodes in state

jsx
// BAD
const [node, setNode] = useState(null);
<div ref={setNode} />        // re-renders on mount with the node

Use useRef unless you specifically need a re-render when the node attaches (rare; useCallback ref is the right escape hatch).

Forwarding refs

To attach a ref to a child component's DOM node, the child must use React.forwardRef:

jsx
const Input = React.forwardRef((props, ref) => <input ref={ref} {...props} />);

React 19 simplifies this — ref becomes a prop directly.

Interview framing

"Refs and state both persist across renders. The defining difference: setting state triggers a re-render; mutating a ref doesn't. Use refs for values that bookkeep but don't drive UI — DOM nodes, timer ids, mutable counters, latest-value mirrors for stale closures, third-party instances, animation per-frame values. Use state when changing the value should update what the user sees. The hard rule: don't read or write refs during render — only in effects and handlers — because React's render must be pure. And don't try to use a ref to 'trigger' a re-render; if you need that, you need state."

Follow-up questions

  • Why don't ref mutations trigger re-renders?
  • When would you reach for a 'latest value' ref?
  • Why is reading/writing refs during render bad?
  • What's a callback ref and when do you use it?

Common mistakes

  • Trying to use a ref to trigger re-render.
  • Mutating ref.current during render.
  • Storing displayed values in refs (UI gets stale).
  • Putting DOM nodes in state (unnecessary re-render).

Performance considerations

  • Refs are zero-cost across renders. Direct DOM writes via refs sidestep React's reconciliation — useful for per-frame animation.

Edge cases

  • Lazy ref initialization (`useRef(() => ...)` not supported; init in effect or check on first read).
  • Refs to functional components — need forwardRef.
  • Concurrent rendering — refs aren't tracked, so don't mutate them during render.

Real-world examples

  • react-window scroll handlers.
  • Mapbox / Three.js wrappers.
  • Form libraries that read inputs imperatively.

Senior engineer discussion

Seniors choose refs vs state by 'does this drive rendering', use refs for latest-value mirrors deliberately, and never mutate refs during render. They reach for direct DOM writes via refs when per-frame updates would otherwise overwhelm React.

Related questions