ESC

Type to search the knowledge base.

useRef for Values and DOM

Mutable boxes that survive renders — DOM refs, instance variables, avoiding re-renders, and the ref-vs-state decision.

beginner6 min read
  • 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:

  1. Hold a DOM node (or external handle) for imperative APIs — focus, scroll, measure, play.
  2. 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

  1. Does changing it need to show up in UI? → state
  2. Is it a DOM node / external instance? → ref
  3. Is it a timer/subscription id? → ref
  4. Do you need previous props/state? → ref (+ effect)
  5. Are you trying to “fix” exhaustive-deps? → pause; maybe restructure

Footguns

  1. Using ref for displayed counters — no re-render.
  2. Reading ref.current in render for DOM measurement — often null on first pass; measure in layout effect or callback ref.
  3. Forgetting cleanup for timers stored in refs.
  4. Assuming ref prop works on custom components without forwardRef / ref-as-prop support.
  5. Storing state in refs to prevent re-renders and then wondering why children don’t update.
  6. Creating useRef(expensive()) — expensive() still runs every render for the argument expression; use lazy init pattern carefully (useRef(null) then assign once in effect, or useState lazy 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).

Further reading

Related guides