Skip to content
React rc hooks 4 min read

useRef

useRef gives a component a small, persistent box whose value survives across renders but never triggers one. Reaching into that box through its .current property lets you imperatively read or mutate data—a DOM node, a timer ID, a previous prop—without entangling it in React’s render cycle. It is the escape hatch you reach for when something needs to live alongside your component without being part of its visual output.

The mutable .current box

Calling useRef(initialValue) returns a plain object of the shape { current: initialValue }. React hands you the same object on every render, so writing to ref.current and reading it later works exactly as you’d expect from an ordinary variable—except it persists between renders the way a normal local variable cannot.

import { useRef } from "react";

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

  return <p>This component has rendered {renderCount.current} time(s).</p>;
}

The crucial property is that mutating .current does not schedule a re-render. The displayed number above only updates when something else causes the component to render again. A ref is for values React doesn’t need to know about to paint the screen.

Avoid reading or writing ref.current during rendering (except for the lazy-initialization pattern). Side effects during render make output unpredictable; do ref work inside event handlers or effects instead.

Referencing a DOM node

The most common use of useRef is grabbing a DOM element. Pass the ref object to a JSX element’s ref attribute and React assigns the underlying DOM node to .current after the element mounts.

import { useRef } from "react";

function SearchBox() {
  const inputRef = useRef(null);

  function focusInput() {
    inputRef.current.focus();
  }

  return (
    <div>
      <input ref={inputRef} type="text" placeholder="Search…" />
      <button onClick={focusInput}>Focus the field</button>
    </div>
  );
}

Before mount and after unmount, inputRef.current is null, so guard against it in code that might run early. DOM refs are the right tool for managing focus, text selection, media playback, measuring layout, or integrating non-React libraries.

Storing timer IDs and other instance values

Anything you’d otherwise stash in a class instance field belongs in a ref. A classic case is holding a timer ID so you can clear it later, since the ID must persist between renders but should never cause one.

import { useRef, useState } from "react";

function Stopwatch() {
  const [seconds, setSeconds] = useState(0);
  const intervalRef = useRef(null);

  function start() {
    if (intervalRef.current !== null) return;
    intervalRef.current = setInterval(() => {
      setSeconds((s) => s + 1);
    }, 1000);
  }

  function stop() {
    clearInterval(intervalRef.current);
    intervalRef.current = null;
  }

  return (
    <div>
      <p>{seconds}s elapsed</p>
      <button onClick={start}>Start</button>
      <button onClick={stop}>Stop</button>
    </div>
  );
}

Tracking the previous value

Because a ref persists across renders, you can record the current value and read the previous one on the next render. Update the ref inside an effect so it reflects the value after the render commits.

import { useRef, useEffect, useState } from "react";

function PriceWatcher({ price }) {
  const prevPriceRef = useRef(price);

  useEffect(() => {
    prevPriceRef.current = price;
  }, [price]);

  const prev = prevPriceRef.current;
  const trend = price > prev ? "up" : price < prev ? "down" : "flat";

  return (
    <p>
      Current: ${price} — trend since last render: {trend}
    </p>
  );
}

Output:

Current: $42 — trend since last render: flat
Current: $45 — trend since last render: up
Current: $40 — trend since last render: down

Why changing a ref does not re-render

State and refs both persist across renders, but they serve opposite purposes. State is reactive: React snapshots it per render and re-renders when you call the setter. A ref is imperative: it is a single mutable object that React deliberately ignores. Updating .current is synchronous and immediately visible—there is no batching, no stale-closure snapshot, and no re-render. That makes refs perfect for values whose changes should be invisible to the UI, and a poor choice for anything the user must see reflected on screen.

Ref vs state

AspectuseRefuseState
Triggers re-render on changeNoYes
Value readref.currentthe state variable directly
MutabilityMutable in placeReplaced via setter
Update timingSynchronous, immediateBatched, applied next render
Survives re-rendersYesYes
Use whenValue shouldn’t affect outputValue drives the UI

The rule of thumb: if rendering depends on the value, use state; if it doesn’t, use a ref.

Lazy initialization

For an expensive initial value, the argument to useRef is evaluated on every render even though only the first result is kept. Sidestep the waste by initializing on first access.

function VideoPlayer() {
  const playerRef = useRef(null);
  if (playerRef.current === null) {
    playerRef.current = createExpensivePlayer();
  }
  // playerRef.current is now stable across renders
}

Best Practices

  • Use refs for values that should persist without affecting the rendered output; reach for useState when the UI must update.
  • Don’t read or write ref.current during rendering—confine ref access to event handlers and effects.
  • Guard DOM refs against null before mount and after unmount.
  • Clear timers, subscriptions, and intervals stored in refs inside effect cleanup to avoid leaks.
  • Don’t list a ref in a dependency array—mutating it doesn’t notify React, so it won’t trigger the effect.
  • To expose a child’s imperative API to a parent, combine useRef with useImperativeHandle rather than passing functions up manually.
Last updated June 14, 2026
Was this helpful?