Saltar al contenido

useRef: la caja que no avisa

¿Por qué mutar una ref no vuelve a renderizar?

Al terminar vas a poder explicar

  • Explicar por qué mutar .current no agenda trabajo
  • Elegir entre estado y ref con un criterio, no por costumbre
  • Detectar el caso en que una ref hace que la UI muestre datos viejos

A useRef se lo suele presentar como «el hook para acceder al DOM». Eso es un caso de uso, no una definición. La definición es más simple y más útil:

useRef devuelve el mismo objeto en todos los renders, y ese objeto no tiene cola de updates.

Todo lo demás sale de ahí.

Un hook sin cola

Volvé a mirar la anatomía de un hook: memoizedState, queue, next. En un useState, la queue es lo que conecta el setter con el planificador de React.

En un useRef, memoizedState es el objeto { current } y queue es null. No hay setter. No hay nada que llame a scheduleUpdateOnFiber. Mutar .current es asignar una propiedad de un objeto común — React ni se entera.

Dos mutaciones de la ref, cero renders

AGENDADOpaso 1 / 121
Traza
Cadena de hooks · <>

Sin hooks en este paso.

Árbol de fibers

  • <HostRoot>

Salida

Nada en el host todavía. React no commiteó ningún cambio.

createRoot().render()

Montaje inicial. No hay árbol previo, así que todo se va a crear desde cero.

VER EL CÓDIGO QUE SE ESTÁ TRAZANDO
1function ConRef() {2  const clicksRef = useRef(0);3  const [mostrado, setMostrado] = useState(0);4 5  return (6    <div>7      <button onClick={() => { clicksRef.current += 1; }}>8        sumar a la ref (no renderiza)9      </button>10      <button onClick={() => setMostrado(clicksRef.current)}>11        traer la ref al estado12      </button>13      <p>mostrado: {mostrado}</p>14    </div>15  );16}

Con el foco acá: ← → un paso · espacio reproduce · Inicio/Fin van a los extremos.

Los dos primeros clicks no producen ni un solo paso de fase render. La traza salta directo del evento a la nota. El tercer click sí renderiza — pero no porque cambió la ref, sino porque su valor se metió en un useState.

Cuándo es la respuesta correcta

  • Identificadores de temporizadores e intervalos. Guardás el id para poder cancelarlo. Nadie lo muestra en pantalla.
  • Nodos del DOM. Los pasás como ref y React los completa.
  • Valores previos. Guardar el valor del render anterior para compararlo.
  • Banderas de «esto ya lo hice». Evitar que un efecto se ejecute dos veces por lógica propia.

La trampa: la UI se queda vieja

El error clásico es guardar en una ref algo que sí se muestra. El valor cambia, la pantalla no, y no hay ningún error que te avise.

1function Contador() {2  const n = useRef(0);3 4  return (5    <button onClick={() => { n.current += 1; }}>6      {n.current}          {/* ✗ nunca se actualiza en pantalla */}7    </button>8  );9}

El n.current sube perfectamente. Lo que no pasa es el render que lo mostraría. Y cuando algún otro estado provoque un render por su cuenta, el número va a saltar de golpe al valor acumulado — lo que hace el bug todavía más confuso.

El criterio es de una sola pregunta: ¿este valor se ve? Si se ve, es estado. Si no se ve, puede ser una ref.

Antes de seguir · predecí

Querés mostrar cuántas veces se renderizó un componente, en pantalla. ¿Ref o estado?

Lo que te llevás

useRef es una caja estable sin cola de updates. Mutarla no agenda trabajo — esa es exactamente su gracia y también su única trampa. Si el valor se muestra, no es una ref.

Antes de marcarla, comprobá

  • ¿Podés explicar explicar por qué mutar .current no agenda trabajo?
  • ¿Podés explicar elegir entre estado y ref con un criterio, no por costumbre?
  • ¿Podés explicar detectar el caso en que una ref hace que la UI muestre datos viejos?