← React & TypeScript

Hooks

¿Qué es useRef y en qué se diferencia de useState?

Ver respuesta — intenta responderla en voz alta primero

useRef te da una caja mutable ({ current: ... }) que sobrevive entre renders pero que NO provoca un re-render cuando cambia. useState también sobrevive, pero cada actualización sí re-renderiza el componente.

Piensa en useRef como una variable de instancia: un lugar donde guardar algo que necesitas recordar entre renders, pero que no afecta lo que se pinta en pantalla.

Los puntos clave:

  • Devuelve siempre el mismo objeto { current: valorInicial } en todos los renders. Puedes leer y escribir ref.current cuando quieras.
  • Cambiar ref.current no re-renderiza. Por eso sirve para IDs de timers, contadores internos, banderas como "¿ya se montó?", o el valor anterior de una prop.
  • Referencias al DOM: si pasas la ref al atributo ref de un elemento JSX, React coloca el nodo real en current después de montar. Así puedes hacer focus, medir tamaños o hacer scroll.
  • Tipado: para el DOM se usa useRef<HTMLInputElement>(null). Empieza en null porque el nodo no existe hasta que React monta; por eso al usarla verificas con ?. o un if.
  • Regla de oro: si el valor se muestra en la UI, es useState. Si solo lo necesitas recordar entre renders, es useRef.
  • No leas ni escribas refs durante el render: hazlo en handlers o efectos, para que el render siga siendo puro.
import { useRef, useState } from "react";

function BuscadorConFoco() {
  // Ref al DOM: empieza en null hasta que React monta el input
  const inputRef = useRef<HTMLInputElement>(null);

  // Ref como valor mutable: cuenta clics SIN provocar renders
  const clicsSinRender = useRef(0);

  const [busqueda, setBusqueda] = useState("");

  function enfocarInput() {
    clicsSinRender.current += 1; // no re-renderiza
    console.log(`Clics acumulados: ${clicsSinRender.current}`);
    inputRef.current?.focus(); // acceso directo al nodo real
  }

  return (
    <div>
      <input
        ref={inputRef}
        value={busqueda}
        onChange={e => setBusqueda(e.target.value)} // esto SÍ re-renderiza
        placeholder="Escribe tu búsqueda"
      />
      <button onClick={enfocarInput}>Enfocar</button>
      {/* Comportamiento esperado: al hacer clic, el input recibe el foco.
          El contador de la ref sube en consola, pero la UI no se repinta
          por ese cambio: solo se repinta cuando cambia "busqueda". */}
    </div>
  );
}

Usar una ref para algo que debe verse en pantalla:

// MAL: la UI nunca se actualiza porque cambiar la ref no re-renderiza
const contador = useRef(0);
return (
  <button onClick={() => { contador.current += 1; }}>
    Clics: {contador.current} {/* se queda congelado en pantalla */}
  </button>
);

// BIEN: si se muestra en la UI, es estado
const [contador, setContador] = useState(0);
return (
  <button onClick={() => setContador(prev => prev + 1)}>
    Clics: {contador}
  </button>
);

El error inverso también existe: usar useState para el ID de un setInterval genera renders inútiles cada vez que lo guardas. Ese ID nadie lo ve: va en una ref.

useRef me da un objeto mutable con una propiedad current que persiste entre renders, pero con una diferencia clave frente a useState: modificar la ref no provoca un re-render. Por eso la uso para valores que necesito recordar pero que no se pintan, como IDs de timers o el valor anterior de una prop. Su otro uso principal es acceder a elementos del DOM: la tipo como useRef<HTMLInputElement>(null), la paso al atributo ref del elemento, y después del montaje puedo hacer focus o medirlo. Mi regla mental es: si el dato aparece en la UI, useState; si solo lo recuerdo entre renders, useRef.

Reto rápido

Crea un componente ReproductorSimple con un <video> y dos botones: "Reproducir" y "Pausar", usando una ref tipada al elemento de video.

Ver solución
import { useRef } from "react";

function ReproductorSimple() {
  // HTMLVideoElement expone play() y pause()
  const videoRef = useRef<HTMLVideoElement>(null);

  return (
    <div>
      <video ref={videoRef} src="/demo.mp4" width={320} />
      <button onClick={() => videoRef.current?.play()}>Reproducir</button>
      <button onClick={() => videoRef.current?.pause()}>Pausar</button>
      {/* Nota: usamos ?. porque current es null hasta que el video monta */}
    </div>
  );
}