useRef Hook

Difficulty: Intermediate

useRef returns a mutable ref object whose .current property is initialized to the passed argument. The returned object persists for the full lifetime of the component. Unlike state, mutating a ref does not cause a re-render. This makes useRef ideal for two distinct purposes: accessing DOM elements and storing mutable values that don't affect the visual output.

The most common use of useRef is to access DOM elements. By passing a ref object as the ref attribute on a JSX element, React sets ref.current to the corresponding DOM node after the component mounts. This lets you imperatively focus inputs, measure element dimensions, scroll to positions, or integrate with third-party DOM libraries. The ref is set to null when the element unmounts.

The second important use is storing mutable instance variables - values that persist across renders but whose changes don't need to trigger re-renders. Common examples include timer IDs, previous prop/state values, flags for tracking mount status, and counters for tracking renders. Think of useRef as a box that can hold any mutable value, similar to instance variables on a class.

A key distinction between useRef and useState is that updating ref.current does not cause a re-render. This is both a feature and a source of bugs. If you need the UI to reflect a value, use state. If you need to track a value without causing re-renders (like a timer ID or a cancelled flag), use a ref. Mixing these up is a common mistake.

Refs are also useful for capturing the previous value of a prop or state variable. By updating the ref in a useEffect, you can compare the current value with the previous one. This pattern is common enough that many codebases extract it into a usePrevious custom hook.

Code examples

DOM ref for input focus

function FocusInput() {
  const inputRef = React.useRef(null);

  React.useEffect(() => {
    console.log('inputRef.current:', inputRef.current ? 'HTMLInputElement' : 'null');
    // In a real app: inputRef.current.focus();
  }, []);

  console.log('Rendering FocusInput');
  // In real JSX: <input ref={inputRef} />
  return null;
}

FocusInput();

useRef(null) creates a ref that will hold a DOM element. The ref is attached via the ref attribute in JSX. In an effect, you can access the DOM node through ref.current.

Mutable ref for render counting

function RenderCounter() {
  const renderCount = React.useRef(0);

  // This runs on every render but does NOT cause re-render
  renderCount.current += 1;

  console.log('Render count:', renderCount.current);
  console.log('Type of renderCount:', typeof renderCount);
  console.log('Has .current:', 'current' in renderCount);

  return null;
}

RenderCounter();

Mutating renderCount.current does not trigger a re-render. The ref persists across renders, making it perfect for tracking values without causing update cycles.

useRef vs useState comparison

function RefVsState() {
  const [stateVal, setStateVal] = React.useState(0);
  const refVal = React.useRef(0);

  // Updating ref does NOT cause re-render
  refVal.current = 42;
  console.log('refVal.current:', refVal.current);
  console.log('stateVal:', stateVal);

  // Updating state DOES cause re-render
  // setStateVal(42) would trigger a re-render

  console.log('Ref update triggers re-render: no');
  console.log('State update triggers re-render: yes');

  return null;
}

RefVsState();

This highlights the key difference: ref mutations are silent (no re-render), while state updates trigger re-renders. Choose based on whether the UI needs to reflect the value.

Key points

Concepts covered

useRef, DOM refs, mutable refs, ref vs state, ref callback