React useRef Hook

useRef gives you a mutable object that persists for the full lifetime of a component. Changing it does not cause a re-render. This makes it useful in two distinct situations: accessing DOM elements directly, and storing values that need to survive re-renders but should not drive the UI.

How useRef works

useRef takes an initial value and returns a ref object with a single property: current.

const ref = useRef(initialValue);
// ref.current === initialValue

You can read and write ref.current at any time. Unlike state, updating it does not schedule a re-render.

Use case 1: Accessing DOM elements

Sometimes you need to work directly with a DOM element: to focus an input, measure its size, scroll to it, or pass it to a third-party library. React normally manages the DOM for you, but refs give you an escape hatch.

To attach a ref to a DOM element, pass it as the ref attribute:

import { useRef } from 'react';

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

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

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

After the component mounts, inputRef.current holds the actual <input> DOM node. You can call any native DOM method on it.

Focusing on mount

A common pattern is to focus an input as soon as the component appears. Combine useRef with useEffect:

import { useRef, useEffect } from 'react';

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

  useEffect(() => {
    inputRef.current.focus();
  }, []);

  return <input ref={inputRef} type="search" placeholder="Search..." />;
}

Reading a value on submit

You can also read an input’s value when a form is submitted, without tracking every keystroke with useState. This is called an uncontrolled input.

import { useRef } from 'react';

export default function SimpleForm() {
  const nameRef = useRef(null);

  function handleSubmit(e) {
    e.preventDefault();
    console.log('Name:', nameRef.current.value);
  }

  return (
    <form onSubmit={handleSubmit}>
      <input ref={nameRef} type="text" placeholder="Your name" />
      <button type="submit">Submit</button>
    </form>
  );
}

For most forms you should use controlled inputs with useState. Refs for form values work well for simple cases or when integrating with non-React code.

Use case 2: Persisting values without re-renders

A ref is also the right tool when you need a value to survive between renders but you do not want changes to that value to trigger a re-render.

Tracking the previous value of state

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

export default function Counter() {
  const [count, setCount] = useState(0);
  const prevCountRef = useRef(0);

  useEffect(() => {
    prevCountRef.current = count;
  });

  const prevCount = prevCountRef.current;

  return (
    <div>
      <p>Current: {count}</p>
      <p>Previous: {prevCount}</p>
      <button onClick={() => setCount(c => c + 1)}>Increment</button>
    </div>
  );
}

The ref stores the previous render’s count value. Because updating a ref does not cause a re-render, this does not create a loop.

Storing a timer ID

When you start a setInterval inside a component, you need to store the ID somewhere so you can cancel it later. State would cause an unnecessary re-render. A ref is the right fit.

import { useState, useRef } from 'react';

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

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

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

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

intervalRef.current holds the interval ID. Reading or updating it has no effect on rendering.

How to update a ref

Assign directly to ref.current. There is no setter function.

ref.current = newValue;

This is intentional. Refs are a deliberate escape from React’s data-flow model. Use them sparingly.

Common mistakes

Reading ref.current during render

When a component first renders, a DOM ref’s current is null. The DOM element does not exist yet. Only access DOM refs inside event handlers or useEffect.

function MyComponent() {
  const ref = useRef(null);

  // Wrong: ref.current is null here during the first render
  console.log(ref.current.value);

  // Correct: read it inside an effect or event handler
  useEffect(() => {
    console.log(ref.current.value);
  }, []);

  return <input ref={ref} />;
}

Using a ref when you need state

If a value changing should update what the user sees, use useState, not useRef. Updating a ref does not re-render the component, so the UI will not reflect the new value.

// Wrong: counter never updates on screen
const countRef = useRef(0);
function handleClick() {
  countRef.current += 1; // UI stays at 0
}

// Correct: counter updates on screen
const [count, setCount] = useState(0);
function handleClick() {
  setCount(c => c + 1); // UI updates
}

What to learn next

  • useState : when you need changes to drive a re-render
  • useEffect : a common companion to useRef for DOM work on mount