React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

refsdommutable-valueshooks

useRef Hook

Hold a mutable value that persists across renders without causing re-renders.

Recipe

Quick-reference recipe card — copy-paste ready.

// Mutable instance variable
const renderCount = useRef(0);
renderCount.current += 1;
 
// DOM element reference
const inputRef = useRef<HTMLInputElement>(null);
inputRef.current?.focus();
 
// Store previous value
const prevValue = useRef(value);
useEffect(() => { prevValue.current = value; });

When to reach for this: You need to access a DOM element directly, store a mutable value that should not trigger re-renders, or keep track of a previous value.

Working Example

"use client";
 
import { useEffect, useRef, useState } from "react";
 
export function Stopwatch() {
  const [elapsed, setElapsed] = useState(0);
  const [running, setRunning] = useState(false);
  const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
 
  useEffect(() => {
    if (running) {
      intervalRef.current = setInterval(() => {
        setElapsed((prev) => prev + 10);
      }, 10);
    }
    return () => {
      if (intervalRef.current) clearInterval(intervalRef.current);
    };
  }, [running]);
 
  const reset = () => {
    setRunning(false);
    setElapsed(0);
  };
 
  const minutes = Math.floor(elapsed / 60000);
  const seconds = Math.floor((elapsed % 60000) / 1000);
  const ms = Math.floor((elapsed % 1000) / 10);
 
  return (
    <div className="space-y-3">
      <p className="text-3xl font-mono tabular-nums">
        {String(minutes).padStart(2, "0")}:{String(seconds).padStart(2, "0")}.
        {String(ms).padStart(2, "0")}
      </p>
      <div className="flex gap-2">
        <button
          onClick={() => setRunning((r) => !r)}
          className="px-3 py-1 border rounded"
        >
          {running ? "Stop" : "Start"}
        </button>
        <button onClick={reset} className="px-3 py-1 border rounded">
          Reset
        </button>
      </div>
    </div>
  );
}

What this demonstrates:

  • Using useRef to store the interval ID so it can be cleared later
  • The ref persists across renders without triggering re-renders when updated
  • ReturnType<typeof setInterval> provides the correct type for the timer ID
  • The ref is cleaned up in the effect's cleanup function

Deep Dive

How It Works

  • useRef returns a mutable object with a single .current property
  • The object reference is stable — React returns the same object on every render
  • Updating .current does not trigger a re-render
  • Unlike useState, there is no setter function — you mutate .current directly
  • The ref object is created once on mount and persists until the component unmounts

Parameters & Return Values

ParameterTypeDescription
initialValueTInitial value assigned to .current
ReturnTypeDescription
ref{ current: T }Mutable ref object

Variations

Focus an input on mount:

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

Track previous value:

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

Store latest callback (avoids stale closures):

const callbackRef = useRef(callback);
useEffect(() => {
  callbackRef.current = callback;
});
 
// Use callbackRef.current in event handlers or timers

Measure a DOM element:

const divRef = useRef<HTMLDivElement>(null);
 
useEffect(() => {
  if (divRef.current) {
    const { width, height } = divRef.current.getBoundingClientRect();
    setSize({ width, height });
  }
}, []);

TypeScript Notes

// DOM ref — use `null` as initial value with element type
const divRef = useRef<HTMLDivElement>(null);
// divRef.current is HTMLDivElement | null
 
// Mutable ref — pass the type as generic
const countRef = useRef<number>(0);
// countRef.current is number
 
// Distinction: useRef<T>(null) creates RefObject<T> (readonly .current)
// useRef<T | null>(null) creates MutableRefObject<T | null>
// For DOM refs, use the first pattern; for mutable values, use the second

Gotchas

  • Reading refs during render — Accessing ref.current during rendering (outside useEffect or event handlers) can give inconsistent results. Fix: Read refs in effects or event handlers only.

  • Expecting re-renders on mutation — Updating ref.current does not re-render the component. Fix: If the UI should update, use useState instead.

  • Null ref on first render — A DOM ref is null until React attaches it after the first render. Fix: Access DOM refs inside useEffect or after a null check.

  • Ref vs. state confusion — Storing UI-visible data in a ref means the display never updates. Fix: Use refs only for values that do not need to be displayed or that drive side effects.

  • Callback refs vs. object refs — Object refs cannot notify you when the element changes (e.g., conditional rendering). Fix: Use a callback ref ref={(node) => { ... }} when you need to react to attachment/detachment.

Alternatives

AlternativeUse WhenDon't Use When
useStateThe value should trigger a re-render when it changesYou need a silent mutable container
Callback refYou need to run code when a ref attaches or detachesYou just need a stable reference to an element
document.getElementByIdOutside React (rare)Inside React components — use refs instead
Module-level variableValue is shared across all component instancesValue should be per-component-instance

Why refs instead of module variables? Module-level variables are shared across all instances of a component. Refs are per-instance — each mounted component gets its own .current.

FAQs

What is the difference between useRef and useState?
  • useRef stores a mutable value that persists across renders but does not trigger re-renders when updated.
  • useState stores a value that triggers a re-render when updated via its setter.
  • Use useRef for values the UI does not display; use useState for values the UI reflects.
Why is my DOM ref null when I try to access it?
  • A DOM ref is null until React attaches it after the first render.
  • Access DOM refs inside useEffect (which runs after mount) or behind a null check.
  • On the first render, the JSX hasn't been committed to the DOM yet.
How do I type a DOM ref vs a mutable value ref in TypeScript?
// DOM ref: use null initial, get RefObject<T> (readonly .current)
const inputRef = useRef<HTMLInputElement>(null);
 
// Mutable ref: include null in the generic union
const countRef = useRef<number | null>(null);
// countRef.current is number | null (mutable)
Gotcha: Why doesn't updating ref.current cause my component to re-render?
  • Updating .current mutates the object in place; React has no way to detect this change.
  • There is no setter function -- you assign directly: ref.current = newValue.
  • If the UI should reflect the value, use useState instead.
How do I store the previous value of a prop or state using useRef?
function usePrevious<T>(value: T): T | undefined {
  const ref = useRef<T | undefined>(undefined);
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
}
  • The ref holds the value from the previous render because useEffect runs after render.
When should I use a callback ref instead of an object ref?
  • Use a callback ref ref={(node) => { ... }} when you need to run code when an element attaches or detaches (e.g., conditional rendering).
  • Object refs only give you the element reference but don't notify you of changes.
  • Callback refs are useful for measuring elements or integrating with non-React libraries.
Can I use useRef to store a timer or interval ID?
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
 
useEffect(() => {
  intervalRef.current = setInterval(tick, 1000);
  return () => {
    if (intervalRef.current) clearInterval(intervalRef.current);
  };
}, []);
  • Yes, this is a common pattern. The ref persists the ID across renders so cleanup can access it.
Gotcha: Is it safe to read ref.current during render?
  • Reading ref.current during render (outside useEffect or event handlers) can give inconsistent results.
  • React may render your component multiple times (concurrent features), and the ref value could be from a different render pass.
  • Read refs only in effects or event handlers.
Why use a ref instead of a module-level variable?
  • Module-level variables are shared across all instances of a component.
  • Refs are per-instance -- each mounted component gets its own .current.
  • If two instances of a component use the same module variable, they overwrite each other.
How do I focus an input on mount using useRef?
const inputRef = useRef<HTMLInputElement>(null);
 
useEffect(() => {
  inputRef.current?.focus();
}, []);
 
return <input ref={inputRef} />;
  • The ref is null until mount, so access it inside useEffect with optional chaining.
What is the "latest callback" ref pattern and why is it useful?
  • Store the latest version of a callback in a ref, updating it on every render.
  • Use callbackRef.current in timers or event listeners to always call the latest version.
  • This avoids stale closures without adding the callback to dependency arrays.
  • useState — when you need the value to trigger re-renders
  • useEffect — access DOM refs inside effects after mount
  • useCallback — stabilize callbacks that are stored in refs
  • useId — generate unique IDs for DOM elements instead of using refs for identification