ReactMedium

Purpose of the useRef hook in React

01

The Short Answer

useRef creates a mutable container that persists across renders without triggering re-renders when changed. It has two primary uses: holding a reference to a DOM element (for imperative operations like focusing an input) and storing mutable values that need to survive re-renders but shouldn't cause them (like timer IDs, previous values, or instance-level flags).

02

How It Works

useRef returns an object with a single property: current. This object is created once on the first render and the same object is returned on every subsequent render. You can read and write .current freely — React doesn't track changes to it, so mutations never trigger re-renders.

basic-ref.tstypescript
const myRef = useRef(initialValue);

// The ref object:
// { current: initialValue }

// Reading:
console.log(myRef.current);

// Writing (does NOT trigger re-render):
myRef.current = newValue;

// Key insight: myRef is the SAME object across all renders
// Only .current changes — the container itself is stable
03

Use Case 1: DOM References

The most common use of useRef is getting a reference to a DOM element for imperative operations — focusing an input, scrolling to an element, measuring dimensions, or integrating with third-party DOM libraries. You attach the ref to a JSX element via the ref attribute, and React sets .current to the DOM node after mounting.

dom-ref.tsxtypescript
function SearchBar() {
  const inputRef = useRef<HTMLInputElement>(null);

  // Focus the input when the component mounts
  useEffect(() => {
    inputRef.current?.focus();
  }, []);

  // Focus programmatically on button click
  function handleClear() {
    setQuery('');
    inputRef.current?.focus(); // Imperative DOM operation
  }

  const [query, setQuery] = useState('');

  return (
    <div>
      <input
        ref={inputRef} // React sets inputRef.current = this DOM node
        value={query}
        onChange={(e) => setQuery(e.target.value)}
      />
      <button onClick={handleClear}>Clear</button>
    </div>
  );
}

Other DOM ref use cases include scrolling an element into view, measuring element dimensions with getBoundingClientRect(), integrating with canvas or video APIs, and attaching third-party libraries that need a DOM node (like chart libraries or map widgets).

scroll-and-measure.tsxtypescript
function MessageList({ messages }: { messages: Message[] }) {
  const bottomRef = useRef<HTMLDivElement>(null);
  const containerRef = useRef<HTMLDivElement>(null);

  // Auto-scroll to bottom when new messages arrive
  useEffect(() => {
    bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
  }, [messages.length]);

  // Measure container height
  function logHeight() {
    const height = containerRef.current?.getBoundingClientRect().height;
    console.log('Container height:', height);
  }

  return (
    <div ref={containerRef} className="overflow-y-auto h-96">
      {messages.map(msg => <MessageBubble key={msg.id} message={msg} />)}
      <div ref={bottomRef} /> {/* Invisible scroll anchor */}
    </div>
  );
}
04

Use Case 2: Mutable Values That Don't Trigger Re-renders

The second major use case is storing values that need to persist across renders but shouldn't cause re-renders when they change. Think of it as an instance variable for function components. Common examples: timer IDs, previous prop values, flags to track whether something has happened, and counters for non-UI purposes.

mutable-values.tsxtypescript
function Stopwatch() {
  const [elapsed, setElapsed] = useState(0);
  const [isRunning, setIsRunning] = useState(false);
  const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);

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

  function stop() {
    setIsRunning(false);
    if (intervalRef.current) {
      clearInterval(intervalRef.current); // Access the timer ID without re-render
      intervalRef.current = null;
    }
  }

  // Cleanup on unmount
  useEffect(() => {
    return () => {
      if (intervalRef.current) clearInterval(intervalRef.current);
    };
  }, []);

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

If you stored the interval ID in state instead of a ref, clearing it would trigger an unnecessary re-render. The ref lets you store and access the ID without any rendering cost.

05

Use Case 3: Tracking Previous Values

A common pattern is using a ref to remember the previous value of a prop or state variable. Since refs persist across renders and don't trigger re-renders, you can update them in an effect to always hold the "last render's" value.

previous-value.tsxtypescript
function usePrevious<T>(value: T): T | undefined {
  const ref = useRef<T | undefined>(undefined);

  useEffect(() => {
    ref.current = value; // Update AFTER render — so during render it holds the old value
  });

  return ref.current; // Returns the value from the PREVIOUS render
}

// Usage
function PriceDisplay({ price }: { price: number }) {
  const previousPrice = usePrevious(price);

  const direction = previousPrice !== undefined
    ? price > previousPrice ? '📈' : price < previousPrice ? '📉' : ''
    : '';

  return <span>{direction} ${price}</span>;
}
06

useRef vs useState

AspectuseRefuseState
Triggers re-render on change?No — silent mutationYes — schedules re-render
Value available immediately?Yes — synchronous write/readNo — new value available next render
Persists across renders?YesYes
Use for UI?No — changes won't show on screenYes — drives what's rendered
Use forDOM refs, timer IDs, flags, previous valuesData that should be reflected in the UI

The decision rule

If changing the value should update what the user sees → useState. If the value is needed internally but shouldn't cause a visual update → useRef. If you find yourself setting state just to store a value you never render, that's a ref.

07

Common Mistakes

👁️

Reading ref.current during render for display

If you read `ref.current` in your JSX to display a value, changes to it won't show up — React doesn't know the ref changed, so it doesn't re-render. The UI shows stale data.

If a value needs to be displayed, use useState. Refs are for values that don't affect the rendered output.

⏱️

Writing to ref.current during render

Mutating a ref during the render phase (outside useEffect) can cause issues with concurrent features and Strict Mode. React may call your component multiple times during render.

Write to refs inside useEffect, event handlers, or callbacks — never during the render phase itself (except for lazy initialization patterns).

08

Why Interviewers Ask This

This question tests whether you understand the distinction between values that drive rendering (state) and values that exist outside the render cycle (refs). Interviewers want to see that you know both use cases (DOM access and mutable containers), understand why refs don't trigger re-renders, can articulate when to use a ref vs state, and know the patterns for common scenarios like timers, previous values, and imperative DOM operations. It reveals your depth of understanding of React's rendering model.

Quick Revision Cheat Sheet

Returns: A mutable object { current: value } that persists across renders

DOM refs: Attach to JSX elements for imperative operations (focus, scroll, measure)

Mutable storage: Timer IDs, flags, counters — anything that shouldn't trigger re-renders

Previous values: Update in useEffect to always hold last render's value

vs useState: Ref = silent, synchronous, no re-render. State = triggers re-render

Rule: Don't read refs during render for display — use state for visible data