C.W.K.
Stream
Lesson 03 of 07 · published

useRef — DOM Refs, Mutable Storage, Callback Refs

~14 min · useref, ref, mutable, imperative

Level 0React Novice
0 XP0/54 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
useRef does three things. Each flavor has its own muscle memory. Conflating them is where bugs live.

Flavor 1 — a handle to a DOM node

You create a ref, hand it to a JSX element via the ref attribute, and after mount the ref's .current property points at the actual DOM node. Read or call DOM methods on it in event handlers and effects. The most common case: focusing an input on mount.

Flavor 2 — render-stable mutable storage

You need a value that survives across renders but doesn't trigger re-renders when it changes. Common uses: caching a timer id between renders, tracking the previous prop value, storing a websocket connection. The shape is identical (useRef(initial)), but you read/write .current directly — no setter, no re-render.

Flavor 3 — callback refs

Sometimes you don't just want the ref's value, you want to react when a node attaches or detaches. Pass a function as the ref instead of a ref object. React calls it with the node on attach, and with null on detach. This is how you wire up an IntersectionObserver, a ResizeObserver, or any third-party library that needs to know about lifecycle.

The distinction that matters

State drives renders. Refs don't. If a value's change should re-render the UI, use state. If a value's change shouldn't re-render but you need to remember it across renders, use a ref. Pick the one that matches what the value's job is.

Don't read .current during render. React doesn't guarantee what's in .current mid-render — DOM refs are null on the first render (the node isn't mounted yet), and reading mutable refs during render is non-deterministic. Always read inside event handlers, effects, or useImperativeHandle.

Code

Flavor 1 — focus an input on mount·tsx
import { useEffect, useRef } from "react";

function AutoFocusInput() {
  const ref = useRef<HTMLInputElement>(null);
  useEffect(() => {
    ref.current?.focus();
  }, []);
  return <input ref={ref} placeholder="Start typing" />;
}
Flavor 2 — mutable storage that doesn't re-render·tsx
import { useEffect, useRef, useState } from "react";

// A 'previous value' hook — the canonical Flavor-2 use case.
function usePrevious<T>(value: T): T | undefined {
  const ref = useRef<T | undefined>(undefined);
  useEffect(() => {
    ref.current = value;
  }, [value]);
  return ref.current;
}

function Counter() {
  const [count, setCount] = useState(0);
  const previousCount = usePrevious(count);
  return (
    <div>
      <p>Now: {count}, before: {previousCount ?? "never"}</p>
      <button onClick={() => setCount((n) => n + 1)}>Increment</button>
    </div>
  );
}
Flavor 3 — callback ref for IntersectionObserver·tsx
import { useCallback } from "react";

function LazySection({ children }: { children: React.ReactNode }) {
  // useCallback keeps the function stable so React doesn't call it on every render.
  const setRef = useCallback((node: HTMLDivElement | null) => {
    if (!node) return; // null = detach; nothing to do
    const observer = new IntersectionObserver(([entry]) => {
      if (entry.isIntersecting) console.log("In view");
    });
    observer.observe(node);
    // (In a real app, you'd also need cleanup — see the exercise.)
  }, []);

  return <div ref={setRef}>{children}</div>;
}

External links

Exercise

Improve the LazySection callback ref above to handle cleanup properly. When the node detaches, you need to disconnect the IntersectionObserver. Hint: store the observer in a useRef (Flavor 2) so the callback can both create on attach and disconnect on detach. Confirm it works by inserting/removing the component dynamically.
Hint
Two refs: one for the observer (useRef<IntersectionObserver | null>(null)), one is the callback ref. On attach, create + observe + store. On detach, read the stored observer and disconnect.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.