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 Hook useRef
  • 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:

  1. Declara inputRef con el Hook useRef.
  2. Pásalo como <input ref={inputRef}>. Esto le dice a React que coloque el nodo DOM <input> en inputRef.current.
  3. En la función handleClick, lee el nodo input del DOM desde inputRef.current y llama a focus() en él con inputRef.current.focus().
  4. Pasa el controlador de evento handleClick a <button> con onClick.

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}>
          Neo
        </button>
        <button onClick={handleScrollToSecondCat}>
          Millie
        </button>
        <button onClick={handleScrollToThirdCat}>
          Bella
        </button>
      </nav>
      <div>
        <ul>
          <li>
            <img
              src="https://placecats.com/neo/300/200"
              alt="Neo"
              ref={firstCatRef}
            />
          </li>
          <li>
            <img
              src="https://placecats.com/millie/200/200"
              alt="Millie"
              ref={secondCatRef}
            />
          </li>
          <li>
            <img
              src="https://placecats.com/bella/199/200"
              alt="Bella"
              ref={thirdCatRef}
            />
          </li>
        </ul>
      </div>
    </>
  );
}

Profundizar

Cómo gestionar una lista de refs usando un callback ref

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, useState } from "react";

export default function CatFriends() {
  const itemsRef = useRef(null);
  const [catList, setCatList] = useState(setupCatList);

  function scrollToCat(cat) {
    const map = getMap();
    const node = map.get(cat);
    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={() => scrollToCat(catList[0])}>Neo</button>
        <button onClick={() => scrollToCat(catList[5])}>Millie</button>
        <button onClick={() => scrollToCat(catList[9])}>Bella</button>
      </nav>
      <div>
        <ul>
          {catList.map((cat) => (
            <li
              key={cat}
              ref={(node) => {
                const map = getMap();
                map.set(cat, node);

                return () => {
                  map.delete(cat);
                };
              }}
            >
              <img src={cat} />
            </li>
          ))}
        </ul>
      </div>
    </>
  );
}

function setupCatList() {
  const catList = [];
  for (let i = 0; i < 10; i++) {
    catList.push("https://loremflickr.com/320/240/cat?lock=" + i);
  }

  return catList;
}

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();
// Add to the Map
map.set(cat, node);

return () => {
// Remove from the Map
map.delete(cat);
};
}}
>

This lets you read individual DOM nodes from the Map later.

Nota

When Strict Mode is enabled, ref callbacks will run twice in development.

Read more about how this helps find bugs in callback refs.

Accediendo a nodos DOM de otros componentes

Atención

Las refs son una vía de escape. Manipular manualmente los nodos DOM de otro componente puede hacer que tu código sea frágil.

Puedes pasar refs desde un componente padre a componentes hijos al igual que cualquier otra prop.

import { useRef } from 'react';

function MyInput({ ref }) {
return <input ref={ref} />;
}

function MyForm() {
const inputRef = useRef(null);
return <MyInput ref={inputRef} />
}

En el ejemplo de arriba, se crea una ref en el componente padre, MyForm, y se pasa al componente hijo, MyInput. MyInput luego pasa la ref a <input>. Debido a que <input> es un componente integrado React establece la propiedad .current de la ref al elemento DOM <input>.

La inputRef creada en MyForm ahora apunta al elemento DOM <input> devuelto por MyInput. Un manejador de clics creado en MyForm puede acceder a inputRef y llamar a focus() para establecer el foco en <input>.

import { useRef } from 'react';

function MyInput({ ref }) {
  return <input ref={ref} />;
}

export default function MyForm() {
  const inputRef = useRef(null);

  function handleClick() {
    inputRef.current.focus();
  }

  return (
    <>
      <MyInput ref={inputRef} />
      <button onClick={handleClick}>
        Enfocar el input
      </button>
    </>
  );
}

Profundizar

Exponiendo un subconjunto de la API con un manejador imperativo

En el ejemplo de arriba, la ref que se pasa MyInput se pasa al 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 hacerlo con useImperativeHandle:

import { useRef, useImperativeHandle } from "react";

function MyInput({ ref }) {
  const realInputRef = useRef(null);
  useImperativeHandle(ref, () => ({
    // Solo expone focus y nada más
    focus() {
      realInputRef.current.focus();
    },
  }));
  return <input ref={realInputRef} />;
};

export default function Form() {
  const inputRef = useRef(null);

  function handleClick() {
    inputRef.current.focus();
  }

  return (
    <>
      <MyInput ref={inputRef} />
      <button onClick={handleClick}>Focus the 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

Vaciando actualizaciones de estado sincrónicamente con flushSync

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 argumento ref 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.