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 algunas tareas que todavía necesitan el elemento en sí: enfocar un input después de que se hace clic en un botón, desplazar un mensaje a la vista, medir el ancho que resultó en una caja, reproducir un video. Ninguna de esas se puede expresar como un trozo de JSX, porque cada una actúa sobre un nodo en lugar de describir uno. Una ref es la válvula de escape para ellas. Le da a un componente una forma de aferrarse a un elemento real del DOM y llamar métodos en él directamente.

Asignar 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}>Enfocar</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 aún no hay un nodo 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í, con cada método y propiedad que el navegador le proporciona, así que inputRef.current.focus() dentro del manejador de clic mueve el cursor al campo. React mantiene la asignación actualizada por ti: si el elemento se elimina de la pantalla, React vuelve a establecer current a null.

Desplazar un nodo a la vista

El enfoque es un trabajo. El desplazamiento es el otro que encontrarás temprano, generalmente en algo como un registro de chat que debe saltar al mensaje más nuevo. Ese trabajo se ejecuta después del render, así 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 hacia lo que desplazarse, que 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 apuntado bottomRef.current a ese div final.

Cambiar una ref nunca desencadena un render

El estado y las refs ambas sobreviven de un render al siguiente, y ahí se detiene el parecido. Llamar a un setter de estado le pide a React que renderice el componente nuevamente con el nuevo valor. Asignar a ref.current cambia el valor en silencio, y la pantalla se queda exactamente como estaba.

Ese silencio es el punto completo. Una ref sostiene algo que la UI no muestra: un nodo del DOM, un ID de temporizador, una bandera que tu lógica de renderizado nunca lee. Volver a renderizar cuando uno de esos cambia te costaría un redraw que no podría cambiar un solo píxel.

Tiene una segunda consecuencia que vale la pena retener. Porque nada tiene que ser programado, el valor en ref.current es legible el instante después de que lo asignas. Una variable de estado es fija durante todo un render y solo se mueve en el siguiente, como cubre State. Una ref se comporta como una caja mutable simple que puedes leer y escribir en cualquier momento.

Cuando una ref es el instinto equivocado

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 state, porque la pantalla tiene que cambiar cuando el valor lo hace. Colócalo en una ref y la actualización pasa desapercibida: el valor se mueve, y la UI sigue mostrando lo que renderizó por última vez.

Entrar en el DOM para cambiar lo que se muestra es el mismo instinto un paso más adelante. Establecer node.textContent manualmente, o cambiar una clase directamente, parece funcionar, y luego el siguiente render lo deshace, porque React vuelve a aplicar lo que tu JSX describe. Las refs cubren dos cosas: leer y comandar un nodo, es decir, enfocarlo, desplazarlo, medirlo, reproducirlo, y sostener valores que sobreviven a través de renders sin ser nunca mostrados. Para cualquier cosa que el usuario lee de la pantalla, descríbela en JSX e impulsa con estado.

El segundo uso de una ref no tiene nada que ver con el DOM. Cualquier valor que tenga que sobrevivir a través de renders sin ser mostrado puede vivir en ref.current, lo que hace de una ref el hogar natural para la gestión de registro, 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}>Iniciar</button>
      <button onClick={stop}>Detener</button>
    </>
  )
}

El ID que setInterval devuelve es la única forma de detener el temporizador más tarde, así que stop tiene que poder encontrarlo. Una variable local se habría ido para el siguiente render, y el estado desencadenaría un re-render inútil cada vez que el temporizador se iniciara. intervalRef.current es la forma correcta para el trabajo, y además funciona como la verificación de "¿ya se está ejecutando?" en start. El efecto al final es la regla de limpieza habitual de Effects: el temporizador se inició aquí, así que algo tiene que detenerlo si el componente abandona la pantalla mientras se está ejecutando.

El mismo patrón te da el valor anterior de una 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 render, así que la ref se actualiza solo después de que el render que usó el valor anterior ha terminado, y el siguiente render lee lo que vino antes. En el primer render, el hook devuelve undefined. Nota 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-render por cada pulsación de tecla, que el capítulo Forms muestra en detalle.

Ser preciso acerca de cuándo ref.current sostiene un nodo explica la mayoría de las sorpresas. React funciona en dos fases. Durante la fase de render llama a tu función de componente para producir una descripción de la UI, 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 desadjunta, ya sea porque el elemento abandonó la pantalla o porque la prop ref ahora apunta a otro lugar, React establece current de nuevo a null antes de adjuntar la nueva.

Entonces el orden es: los efectos y manejadores de eventos ven una ref poblada siempre que el elemento está en la pantalla, y el cuerpo del render no. En el primer render, inputRef.current sigue siendo el null que pasaste a useRef mientras la función se ejecuta. En renders posteriores sostiene 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 render es inseguro. React espera que el renderizado sea puro, y se reserva el derecho de renderizar un componente sin comprometer el resultado: Strict Mode renderiza dos veces en desarrollo, un render concurrente puede ser descartado cuando llega una actualización de mayor prioridad, y un árbol suspendido puede ser reintentado. Una medida tomada durante el render puede describir un diseño que ya no coincide, y una escritura a ref.current durante el render puede suceder dos veces o ser descartada por completo. Mantén lecturas y escrituras en manejadores de eventos, efectos, o refs de callback, donde React ha comprometido 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(() => {
      // reacciona al elemento cambiando de tamaño
    })
    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 desadjunta. Eso supersede la convención más antigua para cualquier callback que devuelva una limpieza, y permite que setup y teardown se sienten juntos como lo hacen en un efecto. Un callback que no devuelve nada sigue siendo invocado una segunda vez con null al desadjuntar, que es la forma que encontrarás en código existente. Un punto afilado: una función de flecha en línea es una función fresca en cada render, así que React desadjunta 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 entregarla a whichever elemento debería recibirla:

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

Expón eso con parsimonia. Entregarle a un padre un nodo DOM en vivo dentro de tu componente hace que el nodo sea parte de tu API pública, y cualquier refactor del markup debajo puede romper la llamada.

Nota de versión

Antes de React 19, los componentes de función no podían recibir una prop ref en absoluto. Pasar una a través de un elemento dentro requería envolver el componente en forwardRef. React 19 entrega ref a componentes de función como una prop ordinaria, así que forwardRef es innecesario en código nuevo y está deprecado. Más atrás, los componentes de clase creaban refs con React.createRef() y las leían desde this. El hook useRef cubre ambos roles en componentes de función.

JunoUna ref alcanza el nodo DOM real Llama useRef(null), pon el resultado en un elemento con ref={inputRef}, y React rellena inputRef.current con el nodo DOM real una vez que está en la pantalla. Entonces puedes hacer cosas con él, como inputRef.current.focus().

Recurre a una ref cuando necesites enfocar, desplazar, o medir algo. Cualquier cosa que la persona que usa tu app lea de la pantalla pertenece al estado.

JunoUna ref alcanza el nodo DOM realuseRef te da una caja con una propiedad current que sobrevive cada render, y escribir en ella nunca programas uno. Eso cubre dos trabajos: alcanzar un nodo DOM para enfoque, desplazamiento, o medición, y guardar valores que la UI nunca muestra, como un ID de temporizador o el valor anterior de una prop.

Si el valor aparece en la pantalla, usa estado para que la pantalla realmente se actualice.

JunoUna ref alcanza el nodo DOM real Las refs se adjuntan durante commit, antes de useLayoutEffect y useEffect, y se desadjuntan de nuevo a null, así que los efectos y manejadores ven un nodo en vivo mientras el cuerpo del render ve el commit anterior. Por eso leer o escribir ref.current durante el render es inseguro: los renders 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 setup con teardown, aunque un callback en línea se vuelve a adjuntar cada render a menos que lo estabilices con useCallback.

Próximo: Hooks, la familia más amplia a la que pertenecen useState, useEffect, y useRef.