Skip to content
Codeloom
React

Mastering useRef in React

Learn how useRef works in React — DOM access, persisting values across renders, and common patterns that avoid unnecessary re-renders.

·5 min read · By Codeloom
Intermediate 10 min read

What you'll learn

  • What useRef does and how it differs from useState
  • How to access and manipulate DOM elements with refs
  • Common patterns for persisting values without re-renders
  • When to use useRef vs useState vs useCallback

Prerequisites

  • Familiarity with React hooks (useState, useEffect)
  • Basic understanding of the React rendering cycle

useRef is one of those hooks that looks simple on the surface but unlocks patterns you cannot achieve with state alone. It gives you a mutable container that persists across renders without triggering a re-render when it changes.

The basics

useRef returns an object with a single property: current.

import { useRef } from 'react';

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

  return <p>This component has rendered {renderCount.current} times.</p>;
}

The key difference from useState: updating ref.current does not cause a re-render. The value is updated immediately, synchronously, and React is completely unaware of the change.

Accessing DOM elements

The most common use case. Pass a ref to any built-in element’s ref prop and React populates current with the underlying DOM node after mount.

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

  function handleClick() {
    inputRef.current.focus();
    inputRef.current.select();
  }

  return (
    <>
      <input ref={inputRef} type="text" placeholder="Click the button..." />
      <button onClick={handleClick}>Focus input</button>
    </>
  );
}

React sets ref.current to null when the element unmounts, so you always get either the real DOM node or null.

Measuring elements

Refs let you measure layout properties the DOM API exposes but React does not.

function MeasuredBox() {
  const boxRef = useRef(null);
  const [dimensions, setDimensions] = useState({ width: 0, height: 0 });

  useEffect(() => {
    if (boxRef.current) {
      const { width, height } = boxRef.current.getBoundingClientRect();
      setDimensions({ width, height });
    }
  }, []);

  return (
    <div ref={boxRef} style={{ padding: '2rem', background: '#f0f0f0' }}>
      This box is {dimensions.width.toFixed(0)}px wide and{' '}
      {dimensions.height.toFixed(0)}px tall.
    </div>
  );
}

Persisting values across renders

Any value that should survive re-renders but should not trigger them belongs in a ref.

Storing a timer ID

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

  function start() {
    if (intervalRef.current) return;
    intervalRef.current = setInterval(() => {
      setElapsed(prev => prev + 1);
    }, 1000);
  }

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

  useEffect(() => {
    return () => clearInterval(intervalRef.current);
  }, []);

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

If you stored the interval ID in state, every setIntervalId(...) call would trigger a re-render for no visible change.

Tracking the previous value

A classic pattern — compare the current prop or state value with its previous value.

function usePrevious(value) {
  const ref = useRef();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
}

function PriceDisplay({ price }) {
  const prevPrice = usePrevious(price);
  const direction = price > prevPrice ? '↑' : price < prevPrice ? '↓' : '—';

  return (
    <span>
      ${price} {direction}
    </span>
  );
}

Callback refs

When you need to run logic at the moment a DOM node attaches or detaches, pass a function instead of a ref object.

function AutoFocusInput() {
  const callbackRef = useCallback((node) => {
    if (node !== null) {
      node.focus();
    }
  }, []);

  return <input ref={callbackRef} type="text" />;
}

Callback refs fire on every mount and unmount. They are useful when the target element is conditionally rendered and useEffect would fire too late.

Forwarding refs to child components

React does not let you pass ref as a prop to a function component. Use forwardRef (or in React 19+, just accept ref as a prop).

const FancyInput = forwardRef(function FancyInput(props, ref) {
  return <input ref={ref} className="fancy" {...props} />;
});

function Parent() {
  const inputRef = useRef(null);
  return <FancyInput ref={inputRef} placeholder="Fancy..." />;
}

In React 19, forwardRef is no longer required — ref is passed as a regular prop:

function FancyInput({ ref, ...props }) {
  return <input ref={ref} className="fancy" {...props} />;
}

useRef vs useState

FeatureuseRefuseState
Triggers re-renderNoYes
Update timingSynchronousBatched, async
Persists across rendersYesYes
Readable in renderYes (but stale if just changed)Yes (current value)

Rule of thumb: if the user needs to see the value on screen, use state. If only your code needs the value, use a ref.

Common mistakes

Reading a ref during render to display its value. The value is correct, but React will not re-render when it changes, so the display goes stale.

// Bug: count shows stale value
function BadCounter() {
  const count = useRef(0);
  return <button onClick={() => count.current++}>{count.current}</button>;
}

Setting a ref in the render body and expecting it to be visible to concurrent features. Refs are escape hatches from React’s model. Keep writes inside effects or event handlers.

Summary

useRef gives you a stable, mutable box that lives for the entire lifetime of a component. Use it for DOM access, timer IDs, previous values, and any data that should not trigger a re-render. Reach for useState the moment the value needs to appear in the UI.