react-hook-useref

How to Use the useRef Hook in React

  • 5 min

We’ve said that in React, the data flow is declarative. You change the state and the interface updates automatically.

However, there are still situations where we need to step outside this flow, to interact imperatively with the browser or to maintain persistence without triggering renders.

This is where we use useRef.

Basically, it’s the “escape hatch” tool that React gives us to manage what we would do with variables without React, without affecting the usual render cycle.

What is useRef?

Technically, a reference (ref) is a mutable container whose value persists throughout the entire lifecycle of the component. The basic syntax is:

const ref = useRef(initialValue);
Copied!
  • initialValue: Initial value of the reference (can be null, a number, an object, etc.).
  • ref.current: Property that stores the current value and can be modified directly.

The key is that this object { current: ... } is the same memory instance throughout the component’s life. React guarantees it will always return the same object, no matter what.

When to use useRef

This Hook has two fundamental purposes that cover the limitations of useState:

  1. Accessing the DOM: Allows getting a direct reference to an HTML node to execute native methods.
  2. Persistence without re-render: Allows storing values that survive re-renders but, when changed, do not cause a new visual update.

Let’s look at each one 👇.

Use 1: Accessing the Real DOM

React is declarative. When we want an <input>, we generally don’t want to touch it. We just link its value to a state. But sometimes we need to touch the DOM element.

The most typical case is managing focus. Imagine you want the cursor to automatically be placed inside an input when the page loads or a button is pressed.

There is no focus={true} prop in HTML that we can easily turn on/off. So we need to call the native .focus() method of the DOM element.

This is where we can use useRef.

Create the reference with an initial value of null.

Connect it to the JSX using the special ref attribute.

Access it using .current when we need it.

import { useRef } from 'react';

export default function FormularioFoco() {
  // 1. Create the ref
  const inputRef = useRef(null);

  const handleClick = () => {
    // 3. Access the real DOM element
    // inputRef.current is the browser's <input> element
    inputRef.current.focus();
    
    // We can do anything native:
    // inputRef.current.scrollIntoView();
    // inputRef.current.style.backgroundColor = 'red'; (Although we shouldn't abuse this)
  };

  return (
    <div>
      {/* 2. Connect the ref to the element */}
      <input ref={inputRef} type="text" />
      <button onClick={handleClick}>
        Focus
      </button>
    </div>
  );
}
Copied!

Do not manipulate the DOM with refs for things you could do with props or state (like changing text or CSS classes).

Use refs only for the “weird stuff” that requires us to use imperative methods (focus, media playback, measuring positions).

Use 2: Mutable Variables (Persistence without render)

Sometimes we need to save a value, but we don’t want changing it to render the component. Let’s see what options we have.

  • let: Resets on every render. Has no memory.
  • useState: Has memory, but triggers a re-render when changed.
  • useRef: Has memory and is silent (does not trigger re-render).

That is, put in table form:

CharacteristicVariable letuseStateuseRef
Persists between renders?❌ No✅ Yes✅ Yes
Has memory?❌ No✅ Yes✅ Yes
Triggers re-render when changed?❌ No✅ Yes❌ No

But let’s see it better with an example. The classic is a stopwatch. We need to save the ID of the setInterval to be able to stop it (clearInterval) later.

But the interval ID is an “internal number”. The user doesn’t care if the ID is 123 or 456. We don’t need to repaint the screen just because that ID changed.

import { useState, useRef } from 'react';

export default function Cronometro() {
  const [segundos, setSegundos] = useState(0);
  
  // We use a ref to store the interval ID
  // If we used useState, we would cause unnecessary extra renders.
  // If we used 'let intervalId', we would lose the value when re-rendering due to seconds.
  const intervalRef = useRef(null);

  const iniciar = () => {
    // Avoid creating multiple intervals
    if (intervalRef.current !== null) return;

    intervalRef.current = setInterval(() => {
      setSegundos(s => s + 1);
    }, 1000);
  };

  const detener = () => {
    if (intervalRef.current) {
      clearInterval(intervalRef.current);
      intervalRef.current = null;
    }
  };

  return (
    <div>
      <h1>Time: {segundos}s</h1>
      <button onClick={iniciar}>Start</button>
      <button onClick={detener}>Stop</button>
    </div>
  );
}
Copied!

In this example, intervalRef.current acts like an instance variable of an old class. It persists, it’s there, we can read and write it, but React ignores it for drawing purposes.

When to use which?

To know if you need useState or useRef, ask yourself this question. “If I change this data, should what is seen on the screen change?”

  • YES ➡ Use useState.
  • NO ➡ Use useRef.

Common mistakes

There is a very serious mistake you must avoid: Reading or writing refs during rendering.

React assumes that the body of your component (the function) must be pure and have no side effects. Modifying a ref is a side effect.

function Componente() {
  const contador = useRef(0);
  
  // ¡ERROR! Modifying ref during render
  contador.current = contador.current + 1;

  return <h1>{contador.current}</h1>; // ¡ERROR! Reading ref in JSX
}
Copied!

Make changes inside Event Handlers or Effects.

function Componente() {
  const contador = useRef(0);
  
  useEffect(() => {
    // Correct: Side effect after render
    contador.current = contador.current + 1;
  });
  
  // ...
}
Copied!