Manipular el DOM con Refs
React automáticamente actualiza el DOM para que coincida con tu salida de renderizado, por lo que tus componentes no necesitarán manipularlo con frecuencia. Sin embargo, a veces es posible que necesites acceder a los elementos del DOM gestionados por React, por ejemplo, enfocar un nodo, desplazarse hasta él, o medir su tamaño y posición. No hay una forma integrada para hacer ese tipo de cosas en React, por lo que necesitarás una ref al nodo DOM.
Aprenderás
- Cómo acceder a un nodo DOM gestionado por React con el atributo
ref
- Cómo el atributo
ref
de JSX se relaciona con el HookuseRef
- Cómo acceder al nodo DOM de otro componente
- En qué casos es seguro modificar el DOM gestionado por React
Obtener una ref del nodo
Para acceder a un nodo DOM gestionado por React, primero importa el Hook useRef
:
import { useRef } from 'react';
Luego, úsalo para declarar una ref dentro de tu componente
const myRef = useRef(null);
Finalmente, pasa la ref como el atributo ref
a la etiqueta JSX en el que quieres obtener el nodo DOM:
<div ref={myRef}>
El Hook useRef
devuelve un objeto con una sola propiedad llamada current
. Inicialmente, myRef.current
va a ser null
. Cuando React cree un nodo DOM para este <div>
, React pondrá una referencia a este nodo en myRef.current
. Entonces podrás acceder a este nodo DOM desde tus controladores de eventos y usar las API de navegador integradas definidas en él.
// Puedes usar cualquier API de navegador, por ejemplo:
myRef.current.scrollIntoView();
Ejemplo: Enfocar el campo de texto input
En este ejemplo, hacer clic en el botón va a enfocar el input:
import { useRef } from 'react'; export default function Form() { const inputRef = useRef(null); function handleClick() { inputRef.current.focus(); } return ( <> <input ref={inputRef} /> <button onClick={handleClick}> Enfocar el input </button> </> ); }
Para implementar esto:
- Declara
inputRef
con el HookuseRef
. - Pásalo como
<input ref={inputRef}>
. Esto le dice a React que coloque el nodo DOM<input>
eninputRef.current
. - En la función
handleClick
, lee el nodo input del DOM desdeinputRef.current
y llama afocus()
en él coninputRef.current.focus()
. - Pasa el controlador de evento
handleClick
a<button>
cononClick
.
Mientras manipular el DOM es el caso de uso más común para las refs, el Hook useRef
puede ser usado para almacenar otras cosas fuera de React, como las ID de temporizadores. De manera similar al estado, las refs permanecen entre renderizados. Las refs son como variables de estado que no desencadenan nuevos renderizados cuando las pones. Lee acerca de las refs en Referenciar valores con refs.
Ejemplo: Desplazarse a un elemento
Puedes tener más de una ref en un componente. En este ejemplo, hay un carrusel de tres imágenes. Cada botón centra una imagen al llamar al método del navegador scrollIntoView()
en el nodo DOM correspondiente:
import { useRef } from 'react'; export default function CatFriends() { const firstCatRef = useRef(null); const secondCatRef = useRef(null); const thirdCatRef = useRef(null); function handleScrollToFirstCat() { firstCatRef.current.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' }); } function handleScrollToSecondCat() { secondCatRef.current.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' }); } function handleScrollToThirdCat() { thirdCatRef.current.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' }); } return ( <> <nav> <button onClick={handleScrollToFirstCat}> Tom </button> <button onClick={handleScrollToSecondCat}> Maru </button> <button onClick={handleScrollToThirdCat}> Jellylorum </button> </nav> <div> <ul> <li> <img src="https://placekitten.com/g/200/200" alt="Tom" ref={firstCatRef} /> </li> <li> <img src="https://placekitten.com/g/300/200" alt="Maru" ref={secondCatRef} /> </li> <li> <img src="https://placekitten.com/g/250/200" alt="Jellylorum" ref={thirdCatRef} /> </li> </ul> </div> </> ); }
Profundizar
En los ejemplos de arriba, hay un número predefinido de refs. Sin embargo, algunas veces es posible que necesites una ref en cada elemento de la lista, y no sabes cuantos vas a tener. Algo como esto no va a funcionar:
<ul>
{items.map((item) => {
// ¡No funciona!
const ref = useRef(null);
return <li ref={ref} />;
})}
</ul>
Esto es porque los Hooks solo tienen que ser llamados en el nivel más alto de tu componente. No puedes llamar a useRef
en un bucle, en una condición, o dentro de una llamada a map()
.
Una posible forma de evitar esto es hacer una sola ref a su elemento padre, y luego usar métodos de manipulación del DOM como querySelectorAll
para «encontrar» los nodos hijos individuales a partir de él. Sin embargo, esto es frágil y puede romperse si la estructura del DOM cambia.
Otra solución es pasar una función al atributo ref
. A esto se le llama un callback ref
. React llamará tu callback ref con el nodo DOM cuando sea el momento de poner la ref, y con null
cuando sea el momento de limpiarla. Esto te permite mantener tu propio array o un Map, y acceder a cualquier ref por su índice o algún tipo de ID.
Este ejemplo te muestra cómo puedes usar este enfoque para desplazarte a un nodo arbitrario en una lista larga:
import { useRef } from 'react'; export default function CatFriends() { const itemsRef = useRef(null); function scrollToId(itemId) { const map = getMap(); const node = map.get(itemId); node.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' }); } function getMap() { if (!itemsRef.current) { // Inicializa el Map en el primer uso itemsRef.current = new Map(); } return itemsRef.current; } return ( <> <nav> <button onClick={() => scrollToId(0)}> Tom </button> <button onClick={() => scrollToId(5)}> Maru </button> <button onClick={() => scrollToId(9)}> Jellylorum </button> </nav> <div> <ul> {catList.map(cat => ( <li key={cat.id} ref={(node) => { const map = getMap(); if (node) { map.set(cat.id, node); } else { map.delete(cat.id); } }} > <img src={cat.imageUrl} alt={'Cat #' + cat.id} /> </li> ))} </ul> </div> </> ); } const catList = []; for (let i = 0; i < 10; i++) { catList.push({ id: i, imageUrl: 'https://placekitten.com/250/200?image=' + i }); }
En este ejemplo, itemsRef
no contiene un solo nodo DOM. En su lugar, contiene un Map desde el ID del elemento hasta un nodo DOM. (¡Las refs pueden contener cualquier valor!) El callback ref
en cada elemento de la lista se encarga de actualizar el Map:
<li
key={cat.id}
ref={node => {
const map = getMap();
if (node) {
// Agregar al Map
map.set(cat.id, node);
} else {
// Eliminar del Map
map.delete(cat.id);
}
}}
>
Esto te permite leer nodos DOM individuales del Map más tarde.
Accediendo a nodos DOM de otros componentes
Cuando colocas una ref en un componente integrado que devuelve de salida un elemento del navegador como <input />
, React establecerá la propiedad current
de esa ref al nodo DOM correspondiente (como el <input />
real del navegador)
Sin embargo, si intentas poner una ref en tu propio componente, como <MyInput />
, por defecto tendrás null
. Aquí hay un ejemplo demostrándolo. Nota como al hacer clic en el botón no enfoca el input.
import { useRef } from 'react'; function MyInput(props) { return <input {...props} />; } export default function MyForm() { const inputRef = useRef(null); function handleClick() { inputRef.current.focus(); } return ( <> <MyInput ref={inputRef} /> <button onClick={handleClick}> Enfocar el input </button> </> ); }
Para ayudarte a notar el problema, React también mostrará un error en la consola.
Esto ocurre porque por defecto React no permite que un componente acceda a los nodos DOM de otros componentes. ¡Ni siquiera a sus propios hijos! Esto es intencionado. Las refs son una vía de escape que debe usarse con moderación. Manipular manualmente los nodos DOM de otro componente hace tu código aún más frágil.
En cambio, los componentes que quieran exponer sus nodos DOM tienen que optar por ese comportamiento. Un componente puede especificar que «reenvíe» su ref a uno de sus hijos. Aquí vemos como MyInput
puede usar la API forwardRef
:
const MyInput = forwardRef((props, ref) => {
return <input {...props} ref={ref} />;
});
Así es como funciona:
<MyInput ref={inputRef} />
le dice a React que coloque el nodo DOM correspondiente eninputRef.current
. Sin embargo, depende del componenteMyInput
utilizarlo o no; por defecto no lo hace.- El componente
MyInput
es declarado usandoforwardRef
. Esto hace que pueda optar por recibir elinputRef
como segundo argumento deref
la cual está declarada después deprops
. MyInput
por si mismo pasa laref
que recibió del<input>
dentro de él.
Ahora al hacer clic en el botón para enfocar el input, funciona:
import { forwardRef, useRef } from 'react'; const MyInput = forwardRef((props, ref) => { return <input {...props} ref={ref} />; }); export default function Form() { const inputRef = useRef(null); function handleClick() { inputRef.current.focus(); } return ( <> <MyInput ref={inputRef} /> <button onClick={handleClick}> Enfocar el input </button> </> ); }
En diseño de sistemas, es un patrón común para componentes de bajo nivel como botones, inputs, etc. reenviar sus refs a sus nodos DOM. Por otro lado, los componentes de alto nivel como formularios, listas, o secciones de página, usualmente no suelen exponer sus nodos DOM para evitar dependencias accidentales de la estructura del DOM.
Profundizar
En el ejemplo de arriba, MyInput
expone el elemento input del DOM original. Esto le permite al componente padre llamar a focus()
en él. Sin embargo, esto también le permite al componente padre hacer otra cosa, por ejemplo, cambiar sus estilos CSS. En casos pocos comunes, quizás quieras restringir la funcionalidad expuesta. Puedes hacer eso con useImperativeHandle
:
import { forwardRef, useRef, useImperativeHandle } from 'react'; const MyInput = forwardRef((props, ref) => { const realInputRef = useRef(null); useImperativeHandle(ref, () => ({ // Solo expone focus y nada más focus() { realInputRef.current.focus(); }, })); return <input {...props} ref={realInputRef} />; }); export default function Form() { const inputRef = useRef(null); function handleClick() { inputRef.current.focus(); } return ( <> <MyInput ref={inputRef} /> <button onClick={handleClick}> Enfocar el input </button> </> ); }
Aquí, realInputRef
dentro de MyInput
mantiene el nodo DOM de input actual. Sin embargo, useImperativeHandle
indica a React a proveer tu propio objeto especial como el valor de una ref al componente padre. Por lo tanto, inputRef.current
dentro del componente Form
solo va a tener el método focus
. En este caso, el «manejador» ref no es el nodo DOM, sino el objeto personalizado que creaste dentro de la llamada de useImperativeHandle
.
Cuando React adjunta las refs
En React, cada actualización está dividida en dos fases:
- Durante el renderizado, React llama a tus componentes para averiguar que debería estar en la pantalla.
- Durante la confirmación, React aplica los cambios a el DOM.
En general, no quieres acceder a las refs durante el renderizado. Eso va también para las refs que tienen nodos DOM. Durante el primer renderizado, los nodos DOM aún no han sido creados, entonces ref.current
será null
. Y durante el renderizado de actualizaciones, los nodos DOM aún no se han actualizado. Es muy temprano para leerlos.
React establece ref.current
durante la confirmación. Antes de actualizar el DOM, React establece los valores afectados de ref.current
a null
. Después de actualizar el DOM, React inmediatamente los establece en los nodos DOM correspondientes.
Generalmente, vas a acceder a las refs desde los controladores de eventos. Si quieres hacer algo con una ref, pero no hay un evento en particular para hacerlo, es posible que necesites un Efecto. Discutiremos los Efectos en las próximas páginas.
Profundizar
Considere un código como el siguiente, que agrega un nuevo todo y desplaza la pantalla hasta el último hijo de la lista. Observa cómo, por alguna razón, siempre se desplaza hacia el todo que estaba justo antes del último que se ha agregado.
import { useState, useRef } from 'react'; export default function TodoList() { const listRef = useRef(null); const [text, setText] = useState(''); const [todos, setTodos] = useState( initialTodos ); function handleAdd() { const newTodo = { id: nextId++, text: text }; setText(''); setTodos([ ...todos, newTodo]); listRef.current.lastChild.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } return ( <> <button onClick={handleAdd}> Agregar </button> <input value={text} onChange={e => setText(e.target.value)} /> <ul ref={listRef}> {todos.map(todo => ( <li key={todo.id}>{todo.text}</li> ))} </ul> </> ); } let nextId = 0; let initialTodos = []; for (let i = 0; i < 20; i++) { initialTodos.push({ id: nextId++, text: 'Todo #' + (i + 1) }); }
El problema está con estas dos lineas:
setTodos([ ...todos, newTodo]);
listRef.current.lastChild.scrollIntoView();
En React, las actualizaciones de estados se ponen en cola. Generalmente, esto es lo que quieres. Sin embargo, aquí causa un problema porque setTodos
no actualiza el DOM inmediatamente. Entonces, en el momento en el que desplazas la lista al último elemento, el todo aún no ha sido agregado. Esta es la razón por la que al desplazarse siempre se «retrasa» en un elemento.
Para arreglar este problema, puedes forzar a React a actualizar («flush») el DOM sincrónicamente. Para hacer esto, importa flushSync
del react-dom
y envuelve el actualizador de estado en una llamada a flushSync
:
flushSync(() => {
setTodos([ ...todos, newTodo]);
});
listRef.current.lastChild.scrollIntoView();
Esto le indicará a React que actualice el DOM sincrónicamente justo después que el código envuelto en flushSync
se ejecute. Como resultado, el último todo ya estará en el DOM en el momento que intentes desplazarte hacia él.
import { useState, useRef } from 'react'; import { flushSync } from 'react-dom'; export default function TodoList() { const listRef = useRef(null); const [text, setText] = useState(''); const [todos, setTodos] = useState( initialTodos ); function handleAdd() { const newTodo = { id: nextId++, text: text }; flushSync(() => { setText(''); setTodos([ ...todos, newTodo]); }); listRef.current.lastChild.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } return ( <> <button onClick={handleAdd}> Agregar </button> <input value={text} onChange={e => setText(e.target.value)} /> <ul ref={listRef}> {todos.map(todo => ( <li key={todo.id}>{todo.text}</li> ))} </ul> </> ); } let nextId = 0; let initialTodos = []; for (let i = 0; i < 20; i++) { initialTodos.push({ id: nextId++, text: 'Todo #' + (i + 1) }); }
Mejores prácticas para la manipulación del DOM con refs
Las refs son una vía de escape. Sólo deberías usarlas cuando tengas que «salirte de React». Ejemplos comunes de esto incluyen la gestión del foco, la posición del scroll, o una llamada a las API del navegador que React no expone.
Si te limitas a acciones no destructivas como enfocar o desplazarte, no deberías encontrar ningún problema. Sin embargo, si intentas modificar el DOM manualmente, puedes arriesgarte a entrar en conflicto con los cambios que React está haciendo.
Para ilustrar este problema, este ejemplo incluye un mensaje de bienvenida y dos botones. El primer botón alterna su presencia usando renderizado condicional y estado, como normalmente lo harías en React. El segundo botón usa la API del DOM remove()
para eliminarlo forzadamente del DOM fuera del control de React.
Intenta presionar «Alternar con setState» unas cuantas veces. El mensaje debe desaparecer y aparecer otra vez. Luego presiona «Eliminar del DOM». Esto lo eliminará forzadamente. Finalmente, presiona «Alternar con setState»:
import { useState, useRef } from 'react'; export default function Counter() { const [show, setShow] = useState(true); const ref = useRef(null); return ( <div> <button onClick={() => { setShow(!show); }}> Alternar con setState </button> <button onClick={() => { ref.current.remove(); }}> Eliminar del DOM </button> {show && <p ref={ref}>Hola Mundo</p>} </div> ); }
Después de que hayas eliminado el elemento DOM, intentar usar setState
para mostrarlo de nuevo provocará un fallo. Esto se debe a que has cambiado el DOM, y React no sabe cómo seguir gestionándolo correctamente.
Evita cambiar nodos DOM gestionados por React. Modificar, agregar hijos, o eliminar hijos de elementos que son gestionados por React pueden traer resultados inconsistentes visuales o fallos como el de arriba.
Sin embargo, esto no quiere decir que no puedas en absoluto. Requiere de cuidado. Puedes modificar de manera segura partes del DOM que React no tenga motivos para actualizar. Por ejemplo, si algún <div>
siempre está vacío en el JSX, React no tendrá un motivo para tocar su lista de elementos hijos. Por lo tanto, es seguro agregar o eliminar manualmente elementos allí.
Recapitulación
- Las refs son un concepto genérico, pero a menudo las vas a usar para almacenar elementos del DOM.
- Tú le indicas a React a poner un nodo DOM dentro de
myRef.current
pasándole<div ref={myRef}>
. - Normalmente, vas a usar las refs para acciones no destructivas como enfocar, desplazar, o medir elementos DOM.
- Un componente no expone sus nodos DOM por defecto. Puedes optar por exponer un nodo DOM usando
forwardRef
y pasando el segundo argumentoref
a un nodo específico. - Evita cambiar nodos DOM gestionados por React.
- Si modificas nodos DOM gestionados por React, modifica las partes en donde React no tenga motivos para actualizar.
Desafío 1 de 4: Reproduce y pausa el video
En este ejemplo, el botón alterna una variable de estado para cambiar entre un estado de reproducción y un estado de pausa. Sin embargo, para que reproduzca o pause el video, alternar el estado no es suficiente. También necesitas llamar a play()
y pause()
en el elemento DOM para el <video>
. Agrega una ref en él, y haz que el botón funcione.
import { useState, useRef } from 'react'; export default function VideoPlayer() { const [isPlaying, setIsPlaying] = useState(false); function handleClick() { const nextIsPlaying = !isPlaying; setIsPlaying(nextIsPlaying); } return ( <> <button onClick={handleClick}> {isPlaying ? 'Pausar' : 'Reproducir'} </button> <video width="250"> <source src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4" type="video/mp4" /> </video> </> ) }
Para un desafío extra, mantén el botón «Reproducir» sincronizado con la reproducción del vídeo, incluso si el usuario hace clic con el botón derecho del ratón en el vídeo y lo reproduce utilizando los controles multimedia integrados en el navegador. Para ello, es posible que quieras escuchar onPlay
y onPause
en el vídeo.