useRef, DOM Refs, and State vs Refs

A ref is a persistent container for a value that React does not use to render the interface. Use it to reach a DOM node or remember mutable information without requesting another render.

The ref mental model

useRef(initialValue) returns the same object on every render. Its one writable property, current, starts with the initial value and can later hold any JavaScript value.

Stable container

The ref object keeps its identity between renders.

Imperative access

A JSX ref can point to a browser DOM node.

Quiet mutation

Changing current does not trigger a render.

A ref survives renders, and changing its current value takes effect immediately.
import { useRef } from "react";

function Example() {
  const visitCount = useRef(0);

  function recordVisit() {
    visitCount.current += 1;
    console.log(visitCount.current); // Updated immediately
  }

  return <button onClick={recordVisit}>Record visit</button>;
}

Do not read or write ref.current while rendering. Rendering should stay pure. Read or change refs in event handlers or Effects instead. Predictable one-time initialization is the narrow exception.

DOM refs

Sometimes code must perform an imperative browser action such as focusing an input, scrolling an element into view, measuring its size, or controlling media. Pass a ref object to a built-in element's ref attribute so React can place that DOM node in current.

Clicking the button focuses the input without storing focus in state.
import { useRef } from "react";

export default function SearchForm() {
  const inputRef = useRef(null);

  function focusSearch() {
    inputRef.current?.focus();
  }

  return (
    <form>
      <label htmlFor="search">Search notes</label>
      <input id="search" ref={inputRef} />
      <button type="button" onClick={focusSearch}>
        Focus search
      </button>
    </form>
  );
}
  1. Start with useRef(null).

    There is no DOM node during the first render, so null is the useful initial value.

  2. Connect the ref in JSX.

    <input ref={inputRef} /> tells React which node the ref should track.

  3. Use the node after React commits it.

    In an event handler, inputRef.current refers to the input. React sets it back to null when that node is removed.

Keep React in charge of structure. Non-destructive actions such as focus, scroll, and measurement are normal ref uses. Avoid manually removing nodes or rewriting content that React renders because the next commit may conflict with those changes.

Mutable non-rendering values

A ref can hold more than a DOM node. It is useful for information that must survive renders but does not describe what the user sees, such as a timeout ID, an external library instance, or bookkeeping used by an event handler.

This button restarts one delayed save. The timeout ID is implementation bookkeeping, so it belongs in a ref.
import { useRef, useState } from "react";

export default function DraftSaver() {
  const [status, setStatus] = useState("Ready");
  const timeoutRef = useRef(null);

  function scheduleSave() {
    clearTimeout(timeoutRef.current);
    setStatus("Waiting to save");

    timeoutRef.current = setTimeout(() => {
      // Send the latest draft to a server here.
      setStatus("Saved");
    }, 1000);
  }

  return (
    <section>
      <button onClick={scheduleSave}>Schedule save</button>
      <p aria-live="polite">{status}</p>
    </section>
  );
}

The user-visible status is state because the paragraph must update. The timeout ID is a ref because React never renders it. Both values persist, but only the state setter requests a new render.

  • Use a ref for information that rendering does not need.

    Ask: would changing this value alone need to update the screen? If not, a ref may fit.

  • Mutate only current, not the ref binding.

    The ref object is stable. Store the changing value in its current property.

  • Clean up external work when needed.

    If a ref tracks a timer, subscription, or third-party object that can outlive the component, cancel or dispose of it in an Effect cleanup.

State vs refs

State and refs both retain values between renders. The deciding question is whether changing the value should make React calculate and commit a new interface.

Use state

Purpose
Data used to produce JSX.
How to update
Call a setter such as setCount.
Rendering behavior
The setter requests a new render. Each render sees a snapshot.
Examples
Typed text, selected tab, validation message, count.

Use a ref

Purpose
Data not needed to produce JSX, or an imperative handle.
How to update
Assign to ref.current.
Rendering behavior
The value changes immediately without requesting a render.
Examples
DOM node, timeout ID, media element, library instance.
A ref is the wrong place for a count displayed in JSX because clicking changes the value without re-rendering the label.
function BrokenCounter() {
  const countRef = useRef(0);

  function handleClick() {
    countRef.current += 1;
    // No render was requested, so the button text stays stale.
  }

  return (
    <button onClick={handleClick}>
      Count: {countRef.current}
    </button>
  );
}

Decision rule: if a value affects rendered output, store it in state. If it only supports an event, browser API, or external system behind the scenes, store it in a ref.

Keep this model: useRef gives a component a stable, mutable container. React can fill it with a DOM node, or your code can keep a non-rendering value in it. Updating current is immediate, but only state updates tell React to render again.

Check your understanding

  1. Why does changing ref.current not update text already displayed on the page?
    Show answer

    React does not track ref mutations as rendering updates. The value changes immediately, but no new render is requested, so existing DOM output remains unchanged.

  2. Why does a DOM ref usually start with null?
    Show answer

    The DOM node does not exist during the first render. React assigns the node to current during the commit and returns it to null when the node is removed.

  3. Should a visible form error message be stored in state or a ref?
    Show answer

    Use state because changing the message must update rendered JSX. A ref is better for related bookkeeping that does not appear in the interface, such as a timeout ID.