Skip to content
This page has been auto-translated and may contain errors.View in English

Refs y el DOM

React construye y actualiza el DOM por ti. Describes cómo debe verse la pantalla, y React se encarga de los elementos reales. Hay una pequeña cantidad de tareas que todavía necesitan el elemento en sí: enfocar un input después de hacer clic en un botón, desplazar un mensaje a la vista, medir el ancho de una caja, reproducir un video. Ninguna de esas cosas se puede expresar como JSX, porque cada una actúa sobre un nodo en lugar de describir uno. Una ref es la puerta de escape para estas tareas. Le da a un componente una forma de mantener una referencia a un elemento del DOM real y llamar métodos en él directamente.

Adjuntar una ref

Una ref comienza con el hook useRef, y pasas el resultado a un elemento a través del atributo ref:

jsx
import { useRef } from 'react'

function SearchBar() {
  const inputRef = useRef(null)

  function handleFocus() {
    inputRef.current.focus()
  }

  return (
    <>
      <input ref={inputRef} />
      <button onClick={handleFocus}>Focus</button>
    </>
  )
}

useRef(null) devuelve un objeto con una sola propiedad, current, que contiene lo que pasaste como valor inicial. Siempre pasas uno, y null es el punto de partida convencional para una ref del DOM, ya que no hay nodo todavía en ese momento.

Escribir ref={inputRef} es la instrucción que React necesita. Cuando React coloca ese <input> en la pantalla, establece inputRef.current al nodo del DOM. A partir de entonces inputRef.current es el elemento input en sí mismo, con cada método y propiedad que el navegador le ofrece, por lo que inputRef.current.focus() dentro del manejador de clic mueve el cursor al campo. React mantiene la asignación actualizada para ti: si el elemento se elimina de la pantalla, React establece current de nuevo en null.

Desplazar un nodo a la vista

Enfocar es una tarea. Desplazarse es la otra que encontrarás rápido, normalmente en algo como un registro de chat que debe saltar al mensaje más reciente. Esa tarea se ejecuta después del renderizado, por lo que se empareja con useEffect:

jsx
import { useRef, useEffect } from 'react'

function MessageList({ messages }) {
  const bottomRef = useRef(null)

  useEffect(() => {
    bottomRef.current.scrollIntoView({ behavior: 'smooth' })
  }, [messages])

  return (
    <div>
      {messages.map(message => (
        <p key={message.id}>{message.text}</p>
      ))}
      <div ref={bottomRef} />
    </div>
  )
}

El <div> vacío al final existe solo como algo a lo que desplazarse, lo cual es un truco común y perfectamente razonable. El efecto se ejecuta cada vez que messages cambia, y para entonces React ya ha colocado los nuevos mensajes en la pantalla y ha apuntado bottomRef.current a ese div final.

Cambiar una ref nunca dispara un renderizado

El estado y las refs ambos persisten de un renderizado al siguiente, y ahí es donde termina el parecido. Llamar a un setter de estado le pide a React que renderice el componente de nuevo con el nuevo valor. Asignar a ref.current cambia el valor silenciosamente, y la pantalla se queda exactamente como estaba.

Ese silencio es el punto completo. Una ref guarda algo que la interfaz de usuario no muestra: un nodo del DOM, un ID de temporizador, una bandera que tu lógica de renderizado nunca lee. Renderizar de nuevo cuando uno de esos cambios te costaría un redibujado que no podría cambiar ni un pixel.

Tiene una segunda consecuencia que vale la pena recordar. Como nada tiene que estar programado, el valor en ref.current es legible al instante después de asignarlo. Una variable de estado está fija durante todo un renderizado y solo se mueve en el siguiente, como explica State. Una ref se comporta como una caja mutable simple que puedes leer y escribir en cualquier momento.

Cuándo una ref es la intuición incorrecta

La línea divisoria es si el valor termina en la pantalla. Texto en un encabezado, un número en una insignia, si un panel está abierto: todo eso pertenece al estado, porque la pantalla tiene que cambiar cuando el valor lo hace. Ponlo en una ref y la actualización pasa desapercibida: el valor se mueve, y la interfaz de usuario sigue mostrando lo que renderizó por última vez.

Alcanzar el DOM para cambiar lo que se muestra es la misma intuición un paso más allá. Establecer node.textContent a mano, o alternar una clase directamente, parece funcionar, y luego el siguiente renderizado lo deshace, porque React vuelve a aplicar lo que tu JSX describe. Las refs cubren dos cosas: leer y controlar un nodo, es decir enfocarlo, desplazarlo, medirlo, reproducirlo, y mantener valores que sobreviven a los renderizados sin ser nunca mostrados. Para cualquier cosa que el usuario lea en la pantalla, descríbela en JSX e impulsa el estado.

El segundo uso de una ref no tiene nada que ver con el DOM. Cualquier valor que tenga que sobrevivir a los renderizados sin ser mostrado puede vivir en ref.current, lo que hace que una ref sea el hogar natural para cosas como un ID de temporizador:

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

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

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

  useEffect(() => () => clearInterval(intervalRef.current), [])

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

El ID que setInterval devuelve es la única forma de detener el temporizador después, por lo que stop tiene que poder encontrarlo. Una variable local desaparecería en el siguiente renderizado, y el estado dispararía un re-renderizado innecesario cada vez que se iniciara el temporizador. intervalRef.current es la forma correcta para el trabajo, y además funciona como la comprobación "¿ya está en funcionamiento?" en start. El efecto al final es la regla de limpieza habitual de Effects: el temporizador se inició aquí, por lo que algo tiene que detenerlo si el componente sale de la pantalla mientras se está ejecutando.

El mismo patrón te da el valor anterior de un prop o una variable de estado, empaquetado aquí como un hook personalizado:

jsx
function usePrevious(value) {
  const ref = useRef(undefined)

  useEffect(() => {
    ref.current = value
  })

  return ref.current
}

El efecto se ejecuta después de cada renderizado, por lo que la ref se actualiza solo después de que se haya completado el renderizado que usó el valor antiguo, y el siguiente renderizado lee lo que vino antes. En el primer renderizado el hook devuelve undefined. Ten en cuenta el useRef(undefined) explícito: useRef toma su valor inicial como argumento, y los tipos de React 19 esperan que ese argumento esté presente, así que pasa undefined deliberadamente cuando no hay nada significativo con lo que comenzar.

Un caso más cotidiano: un campo de formulario no controlado. Cuando un valor solo importa en el momento del envío y nada en la pantalla depende de él mientras el usuario escribe, una ref lo lee directamente del input sin un re-renderizado por pulsación de tecla, lo que el capítulo Forms muestra completo.

Ser preciso acerca de cuándo ref.current contiene un nodo explica la mayoría de las sorpresas. React funciona en dos fases. Durante la fase de renderizado, llama a tu función de componente para producir una descripción de la interfaz de usuario, y aún no existe DOM para esa descripción. Durante la fase de commit, aplica los cambios al DOM real, y es donde se adjuntan las refs: React establece current al nodo que acaba de crear o mantener, luego ejecuta useLayoutEffect, luego deja que el navegador pinte, luego ejecuta useEffect. Cuando una ref se desvincula, ya sea porque el elemento salió de la pantalla o porque la prop ref ahora apunta a otro lugar, React establece current de nuevo en null antes de adjuntar la nueva.

Entonces el orden es: los efectos y los manejadores de eventos ven una ref poblada siempre que el elemento esté en la pantalla, y el cuerpo del renderizado no. En el primer renderizado, inputRef.current sigue siendo el null que pasaste a useRef mientras se ejecuta el cuerpo de la función. En renderizados posteriores contiene el nodo del commit anterior, que describe una pantalla que está a punto de ser reemplazada.

Ese último punto es por qué leer una ref durante el renderizado no es seguro. React espera que el renderizado sea puro, y se reserva el derecho de renderizar un componente sin confirmar el resultado: Strict Mode renderiza dos veces en desarrollo, un renderizado concurrente puede descartarse cuando llega una actualización de mayor prioridad, y un árbol suspendido puede reintentar. Una medida tomada durante el renderizado puede describir un diseño que ya no coincide, y una escritura en ref.current durante el renderizado puede suceder dos veces o descartarse por completo. Mantén las lecturas y escrituras en manejadores de eventos, efectos, o refs de callback, donde React ha confirmado y el tiempo está definido.

Una callback ref es la otra forma de adjuntar una. Pasa una función a ref y React la llama con el nodo al adjuntar:

jsx
<div
  ref={node => {
    const observer = new ResizeObserver(() => {
      // react to the element changing size
    })
    observer.observe(node)
    return () => observer.disconnect()
  }}
/>

En React 19 una callback ref puede devolver una función de limpieza, y React la llama cuando ese nodo se desvincula. Esto reemplaza la convención anterior para cualquier callback que devuelva una limpieza, y permite que la configuración y la desmontada estén juntas como lo hacen en un efecto. Un callback que no devuelve nada sigue siendo invocado una segunda vez con null al desvincularse, que es la forma que encontrarás en código existente. Un detalle afilado: una función flecha en línea es una función nueva en cada renderizado, por lo que React desvincula y vuelve a adjuntar el nodo cada vez. Cuando eso importa, envuelve el callback en useCallback para que su identidad se mantenga estable.

Porque ref llega como una prop ordinaria en React 19, un componente también puede aceptar una y pasarla al elemento que debería recibirla:

jsx
function TextInput({ ref, ...props }) {
  return <input ref={ref} {...props} />
}

Expón eso raramente. Darle a un padre un nodo del DOM vivo dentro de tu componente convierte el nodo en parte de tu API pública, y cualquier refactor del markup debajo puede romper la llamada.

Version note

Before React 19, function components could not receive a ref prop at all. Passing one through to an element inside required wrapping the component in forwardRef. React 19 delivers ref to function components as an ordinary prop, so forwardRef is unnecessary in new code and is deprecated. Further back, class components created refs with React.createRef() and read them from this. The useRef hook covers both roles in function components.

JunoUna ref alcanza el nodo del DOM real Llama a useRef(null), coloca el resultado en un elemento con ref={inputRef}, y React completa inputRef.current con el nodo del DOM actual una vez que está en la pantalla. Luego puedes hacer cosas con él, como inputRef.current.focus(). Recurre a una ref cuando necesites enfocar, desplazar o medir algo. Todo lo que la persona que usa tu app lee en la pantalla pertenece al estado.
JunoUna ref alcanza el nodo del DOM realuseRef te da una caja con una propiedad current que sobrevive a cada renderizado, y escribir en ella nunca programa uno. Eso cubre dos trabajos: alcanzar un nodo del DOM para enfocar, desplazar o medir, y guardar valores que la interfaz de usuario nunca muestra, como un ID de temporizador o el valor anterior de un prop. Si el valor aparece en la pantalla, usa estado para que la pantalla se actualice realmente.
JunoUna ref alcanza el nodo del DOM real Las refs se adjuntan durante el commit, antes de useLayoutEffect y useEffect, y se desvinculan de vuelta a null, por lo que los efectos y manejadores ven un nodo vivo mientras el cuerpo del renderizado ve el commit anterior. Por eso leer o escribir ref.current durante el renderizado no es seguro: los renderizados deben mantenerse puros, y React puede descartar o repetir uno. Las callback refs en React 19 pueden devolver una función de limpieza, que empareja la configuración con la desmontada, aunque un callback en línea se readjunta cada vez a menos que lo estabilices con useCallback.

A continuación: Hooks, la familia más amplia a la que useState, useEffect y useRef pertenecen.