react-hook-useref

Cómo usar el Hook useRef en React

  • 5 min

Hemos dicho que en React, el flujo de datos es declarativo. Tu cambias el estado y la interfaz se actualiza automáticamente.

Sin embargo, siguen existiendo situaciones donde necesitamos salirnos de este flujo, para interactuar imperativamente con el navegador o mantener persistencia sin disparar renderizados.

Aquí es donde utilizamos useRef.

Básicamente es la herramienta de “escape” que nos da React para gestionar lo que sin React haríamos con variables, sin afectar al ciclo de renderizado habitual.

¿Qué es useRef?

Técnicamente una referencia (ref) es un contenedor mutable cuyo valor persiste durante todo el ciclo de vida del componente. La sintaxis básica es:

const ref = useRef(initialValue);
Copied!
  • initialValue: Valor inicial de la referencia (puede ser null, un número, un objeto, etc.).
  • ref.current: Propiedad que almacena el valor actual y puede modificarse directamente.

La clave es que este objeto { current: ... } es la misma instancia de memoria durante toda la vida del componente. React garantiza que te devolverá siempre el mismo objeto, pase lo que pase.

Cuando usar useRef

Este Hook tiene dos propósitos fundamentales que cubren las limitaciones del useState:

  1. Acceso al DOM: Permite obtener una referencia directa a un nodo HTML para ejecutar métodos nativos.
  2. Persistencia sin rerender: Permite almacenar valores que sobreviven a los re-renderizados pero que, al cambiar, no provocan una nueva actualización visual.

Vamos a ver cada uno de ellos 👇.

Uso 1: Acceso al DOM Real

React es declarativo. Cuando querenmos un <input>, generalmente no queremos tocarlo. Sólo vinculamos su valor a un state. Pero a veces necesitamos tocar el elemento del DOM..

El caso más típico es gestionar el foco (focus). Imagina que quieres que, al cargar la página o pulsar un botón, el cursor se coloque automáticamente dentro de un input.

No existe una prop focus={true} en HTML que podamos activar/desactivar fácilmente. Así que necesitamos llamar al método nativo .focus() del elemento DOM.

Aquí es donde podemos usar useRef.

Creamos la referencia con valor inicial null.

La conectamos al JSX usando el atributo especial ref.

Accedemos a ella usando .current cuando la necesitemos.

import { useRef } from 'react';

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

  const handleClick = () => {
    // 3. Acceder al elemento DOM real
    // inputRef.current es el elemento <input> del navegador
    inputRef.current.focus();
    
    // Podemos hacer cualquier cosa nativa:
    // inputRef.current.scrollIntoView();
    // inputRef.current.style.backgroundColor = 'red'; (Aunque no deberíamos abusar de esto)
  };

  return (
    <div>
      {/* 2. Conectar la ref al elemento */}
      <input ref={inputRef} type="text" />
      <button onClick={handleClick}>
        Hacer Foco
      </button>
    </div>
  );
}
Copied!

No manipuléis el DOM con refs para cosas que podríais hacer con props o estado (como cambiar textos o clases CSS).

Usad refs solo para las “cosas raras” que requieren que usemos métodos imperativos (focus, media playback, medir posiciones).

Uso 2: Variables mutables (Persistencia sin render)

A veces necesitamos guardar un valor, pero no queremos que al cambiarlo se renderice el componente. Veamos que opciones tenemos.

  • let: Se resetea en cada renderizado. No tiene memoria.
  • useState: Tiene memoria, pero provoca un re-render al cambiar.
  • useRef: Tiene memoria y es silencioso (no provoca re-render).

Es decir, puesto en versión tablita

CaracterísticaVariable letuseStateuseRef
¿Persiste entre renders?❌ No✅ Sí✅ Sí
¿Tiene memoria?❌ No✅ Sí✅ Sí
¿Provoca re-render al cambiar?❌ No✅ Sí❌ No

Pero lo vamos a ver mejor con un ejemplo. El clásico es un cronómetro. Necesitamos guardar el ID del setInterval para poder pararlo (clearInterval) después.

Pero el ID del intervalo es un “número interno”. Al usuario no le importa si el ID es 123 o 456. No necesitamos repintar la pantalla solo porque ese ID haya cambiado.

import { useState, useRef } from 'react';

export default function Cronometro() {
  const [segundos, setSegundos] = useState(0);
  
  // Usamos una ref para guardar el ID del intervalo
  // Si usáramos useState, provocaríamos renders extra innecesarios.
  // Si usáramos 'let intervalId', perderíamos el valor al re-renderizar por los segundos.
  const intervalRef = useRef(null);

  const iniciar = () => {
    // Evitamos crear múltiples intervalos
    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>Tiempo: {segundos}s</h1>
      <button onClick={iniciar}>Start</button>
      <button onClick={detener}>Stop</button>
    </div>
  );
}
Copied!

En este ejemplo, intervalRef.current actúa como una variable de instancia de una clase antigua. Persiste, está ahí, podemos leerla y escribirla, pero React la ignora a efectos de dibujado.

¿Cuándo usar cuál?

Para saber si necesitáis useState o useRef, haceos esta pregunta. “Si cambio este dato, ¿debe cambiar lo que se ve en la pantalla?”

  • ➡ Usa useState.
  • NO ➡ Usa useRef.

Errores comunes

Hay un error muy grave que debéis evitar: Leer o escribir refs durante el renderizado.

React asume que el cuerpo de vuestro componente (la función) debe ser pura y no tener efectos secundarios. Modificar una ref es un efecto secundario.

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

  return <h1>{contador.current}</h1>; // ¡ERROR! Leyendo ref en el JSX
}
Copied!

Haced los cambios dentro de Event Handlers o Effects.

function Componente() {
  const contador = useRef(0);
  
  useEffect(() => {
    // Correcto: Efecto secundario tras el render
    contador.current = contador.current + 1;
  });
  
  // ...
}
Copied!