React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

refsuseRefforwardRefref-callbacksdom

Refs

Access DOM nodes, store mutable values that persist across renders, and expose handles from child components.

Recipe

Quick-reference recipe card — copy-paste ready.

// DOM ref
const inputRef = useRef<HTMLInputElement>(null);
<input ref={inputRef} />
inputRef.current?.focus();
 
// Mutable value (no re-render on change)
const renderCount = useRef(0);
renderCount.current += 1;
 
// Ref callback (React 19 — supports cleanup)
<div ref={(node) => {
  if (node) {
    // setup
    const observer = new ResizeObserver(() => { /* ... */ });
    observer.observe(node);
    return () => observer.disconnect(); // cleanup
  }
}} />
 
// Forwarding ref (React 19 — ref is a regular prop)
function Input({ ref, ...props }: { ref?: React.Ref<HTMLInputElement> } & React.ComponentPropsWithoutRef<"input">) {
  return <input ref={ref} {...props} />;
}

When to reach for this: You need to interact with the DOM directly (focus, measure, scroll), integrate with a non-React library, or store a value that shouldn't trigger re-renders.

Working Example

"use client";
 
import { useRef, useState, useEffect } from "react";
 
export function AutoFocusSearch() {
  const inputRef = useRef<HTMLInputElement>(null);
  const [query, setQuery] = useState("");
  const [results, setResults] = useState<string[]>([]);
 
  const allItems = ["Apple", "Avocado", "Banana", "Blueberry", "Cherry", "Date", "Fig", "Grape"];
 
  // Focus the input on mount
  useEffect(() => {
    inputRef.current?.focus();
  }, []);
 
  useEffect(() => {
    if (query.trim()) {
      setResults(
        allItems.filter(item =>
          item.toLowerCase().includes(query.toLowerCase())
        )
      );
    } else {
      setResults([]);
    }
  }, [query]);
 
  return (
    <div className="max-w-sm space-y-2 rounded border p-4">
      <div className="flex gap-2">
        <input
          ref={inputRef}
          value={query}
          onChange={e => setQuery(e.target.value)}
          placeholder="Search fruits..."
          className="flex-1 rounded border px-3 py-1"
        />
        <button
          onClick={() => {
            setQuery("");
            inputRef.current?.focus();
          }}
          className="rounded bg-gray-200 px-3 py-1 text-sm"
        >
          Clear
        </button>
      </div>
      {results.length > 0 && (
        <ul className="list-inside list-disc text-sm">
          {results.map(item => (
            <li key={item}>{item}</li>
          ))}
        </ul>
      )}
      {query && results.length === 0 && (
        <p className="text-sm text-gray-400">No results for "{query}"</p>
      )}
    </div>
  );
}
 
// --- Ref Callback with Cleanup (React 19) ---
 
export function MeasuredBox() {
  const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
 
  const measureRef = (node: HTMLDivElement | null) => {
    if (!node) return;
 
    const observer = new ResizeObserver(([entry]) => {
      const { width, height } = entry.contentRect;
      setDimensions({ width: Math.round(width), height: Math.round(height) });
    });
 
    observer.observe(node);
 
    // React 19: return a cleanup function
    return () => observer.disconnect();
  };
 
  return (
    <div className="space-y-2">
      <div
        ref={measureRef}
        className="resize overflow-auto rounded border bg-blue-50 p-4"
        style={{ minWidth: 100, minHeight: 60 }}
      >
        Resize me!
      </div>
      <p className="text-xs text-gray-500">
        {dimensions.width} x {dimensions.height}px
      </p>
    </div>
  );
}

What this demonstrates:

  • useRef to get a handle on an <input> element for programmatic focus
  • Clearing input and re-focusing with inputRef.current?.focus()
  • React 19 ref callback with cleanup function for ResizeObserver
  • Refs for DOM measurement without causing unnecessary re-renders

Deep Dive

How It Works

  • useRef(initialValue) returns a mutable object { current: initialValue } that persists for the full lifetime of the component
  • When passed to a JSX element's ref prop, React sets .current to the DOM node after mounting and back to null on unmount
  • Changing .current does not trigger a re-render — this is the key difference from useState
  • In React 19, ref callbacks can return a cleanup function (like useEffect), which runs when the element unmounts or the ref changes
  • In React 19, ref is a regular prop — no need for forwardRef wrapper

Parameters & Return Values

useRef:

ParameterTypeDescription
initialValueTInitial value for .current
ReturnTypeDescription
refReact.MutableRefObject<T>Object with a mutable .current property

Ref Types Compared

TypeCreated ByUse Case
React.RefObject<T>useRef<T>(null)DOM element references
React.MutableRefObject<T>useRef<T>(value)Mutable instance variables (timers, previous values)
React.Ref<T>Callback or objectAccepting refs as props (union of callback ref and ref object)

Variations

Storing previous value:

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

Timer ref (avoiding stale closures):

function Stopwatch() {
  const [elapsed, setElapsed] = useState(0);
  const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
 
  function start() {
    if (intervalRef.current) return;
    intervalRef.current = setInterval(() => {
      setElapsed(prev => prev + 1);
    }, 1000);
  }
 
  function stop() {
    if (intervalRef.current) {
      clearInterval(intervalRef.current);
      intervalRef.current = null;
    }
  }
 
  useEffect(() => {
    return () => stop(); // cleanup on unmount
  }, []);
 
  return (
    <div>
      <span>{elapsed}s</span>
      <button onClick={start}>Start</button>
      <button onClick={stop}>Stop</button>
    </div>
  );
}

Forwarding refs in React 19 (no forwardRef needed):

interface FancyInputProps {
  label: string;
  ref?: React.Ref<HTMLInputElement>;
}
 
function FancyInput({ label, ref, ...props }: FancyInputProps) {
  return (
    <label>
      {label}
      <input ref={ref} {...props} />
    </label>
  );
}
 
// Parent
function Form() {
  const inputRef = useRef<HTMLInputElement>(null);
  return (
    <>
      <FancyInput label="Name" ref={inputRef} />
      <button onClick={() => inputRef.current?.focus()}>Focus</button>
    </>
  );
}

useImperativeHandle (exposing a custom API):

import { useRef, useImperativeHandle } from "react";
 
interface ModalHandle {
  open: () => void;
  close: () => void;
}
 
function Modal({ ref, children }: { ref?: React.Ref<ModalHandle>; children: React.ReactNode }) {
  const dialogRef = useRef<HTMLDialogElement>(null);
 
  useImperativeHandle(ref, () => ({
    open: () => dialogRef.current?.showModal(),
    close: () => dialogRef.current?.close(),
  }));
 
  return <dialog ref={dialogRef}>{children}</dialog>;
}
 
// Parent
function App() {
  const modalRef = useRef<ModalHandle>(null);
  return (
    <>
      <button onClick={() => modalRef.current?.open()}>Open</button>
      <Modal ref={modalRef}>
        <p>Hello!</p>
        <button onClick={() => modalRef.current?.close()}>Close</button>
      </Modal>
    </>
  );
}

TypeScript Notes

// DOM ref — pass null as initial value, type the element
const divRef = useRef<HTMLDivElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
 
// Mutable ref — non-null initial value
const countRef = useRef<number>(0); // MutableRefObject<number>
 
// Accepting a ref prop in React 19
interface Props {
  ref?: React.Ref<HTMLInputElement>;
}
 
// Typing useImperativeHandle
interface Handle {
  scrollToTop: () => void;
}
useImperativeHandle(ref, (): Handle => ({
  scrollToTop: () => window.scrollTo(0, 0),
}));

Gotchas

  • Reading ref during renderinputRef.current is null during the first render because the DOM node doesn't exist yet. Fix: Access refs in event handlers, useEffect, or after a null check.

  • Ref changes don't trigger re-renders — Updating ref.current will not cause the component to re-render or reflect changes in JSX. Fix: If you need the UI to update, use useState instead. Use refs only for values the render output doesn't depend on.

  • forwardRef is deprecated in React 19React.forwardRef still works but is no longer necessary. Fix: Accept ref as a regular prop: function MyComp({ ref }: { ref?: React.Ref<HTMLElement> }).

  • Assigning ref to a conditional element — If the element with ref is conditionally rendered, .current will be null when the element is hidden. Fix: Always null-check before using: ref.current?.focus().

  • Ref callback firing twice in StrictMode — In development with StrictMode, ref callbacks fire with null then the node, simulating mount/unmount/remount. Fix: This is expected — ensure your ref callback handles null gracefully. In React 19, return a cleanup function instead of checking for null.

Alternatives

AlternativeUse WhenDon't Use When
useStateThe value needs to trigger re-renders when it changesYou're storing a timer ID, previous value, or DOM node
document.getElementByIdQuick prototype outside ReactProduction React code (breaks component encapsulation)
Data attributes + CSSYou need to toggle styles based on stateYou need programmatic DOM access (focus, scroll, measure)
useImperativeHandleYou want to expose a limited API from a child componentYou just need the raw DOM node

FAQs

What is useRef and when should I use it?

useRef returns a mutable object { current: value } that persists across renders without causing re-renders. Use it for DOM access (focus, scroll, measure), storing timer IDs, and keeping mutable values that the UI doesn't depend on.

What is the difference between useRef and useState?
  • useState triggers a re-render when updated — use for values the UI displays
  • useRef does not trigger re-renders — use for values like timer IDs, previous values, or DOM nodes
Why is ref.current null on the first render?

DOM refs are populated after React mounts the element. During render, the DOM node doesn't exist yet. Access refs in event handlers, useEffect, or behind a null check: ref.current?.focus().

Do I still need forwardRef in React 19?

No. In React 19, ref is a regular prop. Accept it directly:

function Input({ ref, ...props }: { ref?: React.Ref<HTMLInputElement> }) {
  return <input ref={ref} {...props} />;
}
What is a ref callback and how is it different from useRef?

A ref callback is a function passed to the ref prop. React calls it with the DOM node on mount and null on unmount. In React 19, it can return a cleanup function. Use it when you need setup/teardown logic tied to the DOM node (like ResizeObserver).

How do I store a timer ID with useRef?
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
timerRef.current = setInterval(() => { ... }, 1000);
// Later:
clearInterval(timerRef.current!);

This avoids stale closure issues since ref.current always points to the latest value.

What is useImperativeHandle?

It lets you customize the value exposed when a parent uses a ref on your component. Instead of exposing the raw DOM node, you expose a limited API like { open(), close() }. Use sparingly — prefer props for most communication.

Can I use useRef to track how many times a component has rendered?

Yes. Increment ref.current in the component body:

const renderCount = useRef(0);
renderCount.current += 1;

Since ref changes don't trigger re-renders, this won't cause an infinite loop.

Why does my ref callback fire twice in StrictMode?

React's StrictMode simulates unmount/remount in development, so the ref callback fires with null then the node. This is expected. In React 19, return a cleanup function from the ref callback instead of checking for null.

How do I create a usePrevious custom hook with useRef?
function usePrevious<T>(value: T): T | undefined {
  const ref = useRef<T | undefined>(undefined);
  useEffect(() => { ref.current = value; }, [value]);
  return ref.current;
}

The ref stores the old value because useEffect runs after render.

  • Forms — using refs for uncontrolled form inputs
  • Events — attaching native event listeners via refs
  • Components — composing components that accept refs
  • useState — when you need reactivity instead of a ref