Manipulating the DOM with Refs

O React atualiza automaticamente o DOM para corresponder à saída do seu render, então seus componentes não precisarão manipulá-lo com frequência. No entanto, às vezes você pode precisar acessar os elementos DOM gerenciados pelo React — por exemplo, para focar um nó, rolar até ele ou medir seu tamanho e posição. Não há uma maneira integrada de fazer essas coisas no React, então você precisará de uma ref para o nó DOM.

Você aprenderá

  • Como acessar um nó DOM gerenciado pelo React com o atributo ref
  • Como o atributo JSX ref se relaciona com o Hook useRef
  • Como acessar o nó DOM de outro componente
  • Em quais casos é seguro modificar o DOM gerenciado pelo React

Obtendo uma ref para o nó

Para acessar um nó DOM gerenciado pelo React, primeiro, importe o Hook useRef:

import { useRef } from 'react';

Em seguida, use-o para declarar uma ref dentro do seu componente:

const myRef = useRef(null);

Finalmente, passe sua ref como o atributo ref para a tag JSX para a qual você deseja obter o nó DOM:

<div ref={myRef}>

O Hook useRef retorna um objeto com uma única propriedade chamada current. Inicialmente, myRef.current será null. Quando o React criar um nó DOM para este <div>, o React colocará uma referência a este nó em myRef.current. Você pode então acessar este nó DOM a partir dos seus manipuladores de eventos e usar as APIs do navegador integradas definidas nele.

// Você pode usar quaisquer APIs do navegador, por exemplo:
myRef.current.scrollIntoView();

Exemplo: Focando um campo de texto

Neste exemplo, clicar no botão focará no campo de entrada:

import { useRef } from 'react';

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

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

  return (
    <>
      <input ref={inputRef} />
      <button onClick={handleClick}>
        Focus the input
      </button>
    </>
  );
}

Para implementar isso:

  1. Declare inputRef com o Hook useRef.
  2. Passe-o como <input ref={inputRef}>. Isso diz ao React para colocar o nó DOM deste <input> em inputRef.current.
  3. Na função handleClick, leia o nó DOM do input a partir de inputRef.current e chame focus() nele com inputRef.current.focus().
  4. Passe o manipulador de eventos handleClick para <button> com onClick.

Embora a manipulação do DOM seja o caso de uso mais comum para refs, o Hook useRef pode ser usado para armazenar outras coisas fora do React, como IDs de timers. De forma semelhante ao estado, as refs persistem entre as renderizações. Refs são como variáveis de estado que não disparam re-renderizações quando você as define. Leia sobre refs em Referenciando Valores com Refs.

Exemplo: Rolando para um elemento

Você pode ter mais de uma ref em um componente. Neste exemplo, há um carrossel de três imagens. Cada botão centraliza uma imagem chamando o método do navegador scrollIntoView() no nó DOM correspondente:

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>
    </>
  );
}

Deep Dive

Como gerenciar uma lista de refs usando um callback de ref

Nos exemplos acima, há um número predefinido de refs. No entanto, às vezes você pode precisar de uma ref para cada item da lista, e você não sabe quantos terá. Algo como isto não funcionaria:

<ul>
{items.map((item) => {
// Não funciona!
const ref = useRef(null);
return <li ref={ref} />;
})}
</ul>

Isso ocorre porque Hooks só podem ser chamados no nível superior do seu componente. Você não pode chamar useRef em um loop, em uma condição ou dentro de uma chamada map().

Uma maneira possível de contornar isso é obter uma única ref para o elemento pai e, em seguida, usar métodos de manipulação do DOM como querySelectorAll para “encontrar” os nós filhos individuais a partir dele. No entanto, isso é frágil e pode quebrar se a sua estrutura DOM mudar.

Outra solução é passar uma função para o atributo ref. Isso é chamado de callback de ref. O React chamará seu callback de ref com o nó DOM quando for hora de definir a ref e chamará a função de limpeza retornada do callback quando for hora de limpá-la. Isso permite que você mantenha seu próprio array ou um Map, e acesse qualquer ref por seu índice ou algum tipo de ID.

Este exemplo mostra como você pode usar essa abordagem para rolar até um nó arbitrário em uma lista longa:

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 o Map no primeiro 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[8])}>Bella</button>
      </nav>
      <div>
        <ul>
          {catList.map((cat) => (
            <li
              key={cat.id}
              ref={(node) => {
                const map = getMap();
                map.set(cat, node);

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

function setupCatList() {
  const catCount = 10;
  const catList = new Array(catCount)
  for (let i = 0; i < catCount; i++) {
    let imageUrl = '';
    if (i < 5) {
      imageUrl = "https://placecats.com/neo/320/240";
    } else if (i < 8) {
      imageUrl = "https://placecats.com/millie/320/240";
    } else {
      imageUrl = "https://placecats.com/bella/320/240";
    }
    catList[i] = {
      id: i,
      imageUrl,
    };
  }
  return catList;
}

Neste exemplo, itemsRef não contém um único nó DOM. Em vez disso, ele contém um Map do ID do item para um nó DOM. (Refs podem conter quaisquer valores!) O callback de ref em cada item da lista cuida de atualizar o Map:

<li
key={cat.id}
ref={node => {
const map = getMap();
// Adiciona ao Map
map.set(cat, node);

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

Isso permite que você leia nós DOM individuais do Map posteriormente.

Note

Quando o Strict Mode está ativado, os callbacks de ref serão executados duas vezes em desenvolvimento.

Leia mais sobre como isso ajuda a encontrar bugs em callbacks de ref.

Acessando nós DOM de outros componentes

Pitfall

Refs são uma “escape hatch”. Manipular manualmente os nós DOM de outro componente pode tornar seu código frágil.

Você pode passar refs de um componente pai para componentes filhos assim como qualquer outra prop.

import { useRef } from 'react';

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

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

No exemplo acima, uma ref é criada no componente pai, MyForm, e é passada para o componente filho, MyInput. MyInput então passa a ref para <input>. Como <input> é um componente integrado, o React define a propriedade .current da ref para o elemento DOM <input>.

A inputRef criada em MyForm agora aponta para o elemento DOM <input> retornado por MyInput. Um manipulador de clique criado em MyForm pode acessar inputRef e chamar focus() nele para focar em <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}>
        Focus the input
      </button>
    </>
  );
}

Deep Dive

Expondo um subconjunto da API com um handle imperativo

No exemplo acima, a ref passada para MyInput é passada para o elemento de input DOM original. Isso permite que o componente pai chame focus() nele. No entanto, isso também permite que o componente pai faça outra coisa — por exemplo, alterar seus estilos CSS. Em casos incomuns, você pode querer restringir a funcionalidade exposta. Você pode fazer isso com useImperativeHandle:

import { useRef, useImperativeHandle } from "react";

function MyInput({ ref }) {
  const realInputRef = useRef(null);
  useImperativeHandle(ref, () => ({
    // Expõe apenas o foco e nada mais
    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>
    </>
  );
}

Aqui, realInputRef dentro de MyInput contém o nó DOM do input real. No entanto, useImperativeHandle instrui o React a fornecer seu próprio objeto especial como o valor de uma ref para o componente pai. Portanto, inputRef.current dentro do componente Form terá apenas o método focus. Neste caso, o “handle” da ref não é o nó DOM, mas o objeto personalizado que você cria dentro da chamada useImperativeHandle.

Quando o React anexa as refs

No React, cada atualização é dividida em duas fases:

  • Durante o render, o React chama seus componentes para descobrir o que deve estar na tela.
  • Durante o commit, o React aplica as alterações ao DOM.

Em geral, você não quer acessar refs durante a renderização. Isso vale também para refs que contêm nós DOM. Durante a primeira renderização, os nós DOM ainda não foram criados, então ref.current será null. E durante a renderização de atualizações, os nós DOM ainda não foram atualizados. Portanto, é muito cedo para lê-los.

O React define ref.current durante o commit. Antes de atualizar o DOM, o React define os valores ref.current afetados como null. Após atualizar o DOM, o React os define imediatamente para os nós DOM correspondentes.

Geralmente, você acessará refs a partir de manipuladores de eventos. Se você quiser fazer algo com uma ref, mas não houver um evento específico para isso, você pode precisar de um Effect. Discutiremos Effects nas próximas páginas.

Deep Dive

Descarregando atualizações de estado de forma síncrona com flushSync

Considere um código como este, que adiciona um novo item de tarefa e rola a tela para baixo até o último filho da lista. Observe como, por algum motivo, ele sempre rola para o item de tarefa que estava imediatamente antes do último adicionado:

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}>
        Add
      </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)
  });
}

O problema está nestas duas linhas:

setTodos([ ...todos, newTodo]);
listRef.current.lastChild.scrollIntoView();

No React, as atualizações de estado são enfileiradas. Geralmente, é isso que você quer. No entanto, aqui isso causa um problema porque setTodos não atualiza o DOM imediatamente. Portanto, no momento em que você rola a lista para seu último elemento, o item de tarefa ainda não foi adicionado. É por isso que a rolagem sempre fica “atrasada” em um item.

Para corrigir esse problema, você pode forçar o React a atualizar (“descarregar”) o DOM de forma síncrona. Para fazer isso, importe flushSync de react-dom e envolva a atualização de estado em uma chamada flushSync:

flushSync(() => {
setTodos([ ...todos, newTodo]);
});
listRef.current.lastChild.scrollIntoView();

Isso instruirá o React a atualizar o DOM de forma síncrona logo após a execução do código envolvido em flushSync. Como resultado, o último item de tarefa já estará no DOM quando você tentar rolar até ele:

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}>
        Add
      </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)
  });
}

Melhores práticas para manipulação do DOM com refs

Refs são uma “válvula de escape”. Você só deve usá-las quando precisar “sair do React”. Exemplos comuns incluem gerenciar foco, posição de rolagem ou chamar APIs do navegador que o React não expõe.

Se você se ater a ações não destrutivas como focar e rolar, não deverá encontrar problemas. No entanto, se você tentar modificar o DOM manualmente, corre o risco de conflitar com as alterações que o React está fazendo.

Para ilustrar esse problema, este exemplo inclui uma mensagem de boas-vindas e dois botões. O primeiro botão alterna sua presença usando renderização condicional e estado, como você faria normalmente no React. O segundo botão usa a API DOM remove() para removê-lo forçosamente do DOM fora do controle do React.

Tente pressionar “Toggle with setState” algumas vezes. A mensagem deve desaparecer e reaparecer. Em seguida, pressione “Remove from the DOM”. Isso o removerá à força. Finalmente, pressione “Toggle with 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);
        }}>
        Toggle with setState
      </button>
      <button
        onClick={() => {
          ref.current.remove();
        }}>
        Remove from the DOM
      </button>
      {show && <p ref={ref}>Hello world</p>}
    </div>
  );
}

Depois de remover manualmente o elemento DOM, tentar usar setState para mostrá-lo novamente levará a um erro. Isso ocorre porque você alterou o DOM, e o React não sabe como continuar gerenciando-o corretamente.

Evite alterar nós DOM gerenciados pelo React. Modificar, adicionar filhos ou remover filhos de elementos que são gerenciados pelo React pode levar a resultados visuais inconsistentes ou erros como o acima.

No entanto, isso não significa que você não possa fazer isso. Requer cautela. Você pode modificar com segurança partes do DOM que o React não tem razão para atualizar. Por exemplo, se um <div> estiver sempre vazio no JSX, o React não terá motivo para tocar em sua lista de filhos. Portanto, é seguro adicionar ou remover elementos manualmente lá.

Recap

  • Refs são um conceito genérico, mas na maioria das vezes você as usará para manter elementos DOM.
  • Você instrui o React a colocar um nó DOM em myRef.current passando <div ref={myRef}>.
  • Geralmente, você usará refs para ações não destrutivas como focar, rolar ou medir elementos DOM.
  • Um componente não expõe seus nós DOM por padrão. Você pode optar por expor um nó DOM usando a prop ref.
  • Evite alterar nós DOM gerenciados pelo React.
  • Se você modificar nós DOM gerenciados pelo React, modifique partes que o React não tem motivo para atualizar.

Challenge 1 of 4:
Reproduzir e pausar o vídeo

Neste exemplo, o botão alterna uma variável de estado para alternar entre um estado de reprodução e pausa. No entanto, para realmente reproduzir ou pausar o vídeo, alternar o estado não é suficiente. Você também precisa chamar play() e pause() no elemento DOM para o <video>. Adicione uma ref a ele e faça o botão funcionar.

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 ? 'Pause' : 'Play'}
      </button>
      <video width="250">
        <source
          src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
          type="video/mp4"
        />
      </video>
    </>
  )
}

Para um desafio extra, mantenha o botão “Play” sincronizado com o fato de o vídeo estar sendo reproduzido, mesmo que o usuário clique com o botão direito do mouse no vídeo e o reproduza usando os controles de mídia integrados do navegador. Você pode querer ouvir onPlay e onPause no vídeo para fazer isso.