Manipulando o DOM com Refs

O React atualiza automaticamente o DOM para corresponder à sua saída de renderização, então seus componentes não precisarão frequentemente manipulá-lo. No entanto, às vezes você pode precisar de acesso aos elementos DOM gerenciados pelo React—por exemplo, para focar um nó, rolar para 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 um ref para o nó do DOM.

Você aprenderá

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

Obtendo um ref para o nó

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

import { useRef } from 'react';

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

const myRef = useRef(null);

Finalmente, passe seu ref como o atributo ref para a tag JSX da qual você deseja obter o nó do 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ó do DOM para este <div>, o React colocará uma referência a este nó em myRef.current. Você poderá então acessar este nó do DOM de seus manipuladores de eventos e usar as APIs de navegador integradas definidas nele.

// Você pode usar qualquer API de navegador, por exemplo:
myRef.current.scrollIntoView();

Exemplo: Focando um campo de texto

Neste exemplo, clicar no botão irá focar a 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}>
        Focar na entrada
      </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ó do DOM deste <input> em inputRef.current.
  3. Na função handleClick, leia o nó do DOM da entrada 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 temporizador. Assim como o estado, os refs permanecem entre as renderizações. Os refs são como variáveis de estado que não acionam novas renderizações quando você as define. Leia sobre refs em Referenciando Valores com Refs.

Exemplo: Rolando para um elemento

Você pode ter mais de um ref em um componente. Neste exemplo, há um carrossel de três imagens. Cada botão centraliza uma imagem chamando o método scrollIntoView() do navegador 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}>
          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>
    </>
  );
}

Deep Dive

Como gerenciar uma lista de refs usando um callback ref

Nos exemplos acima, há um número pré-definido de refs. No entanto, às vezes você pode precisar de um ref para cada item na 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 acontece porque os Hooks devem ser chamados apenas 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 possível maneira de contornar isso é obter um único ref para seu 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 sua estrutura DOM mudar.

Outra solução é passar uma função para o atributo ref. Isso é chamado de callback ref. O React chamará seu callback ref com o nó DOM quando for hora de definir o ref, e com null quando for hora de limpá-lo. 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 para 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 na primeira utilização.
      itemsRef.current = new Map();
    }
    return itemsRef.current;
  }

  return (
    <>
      <nav>
        <button onClick={() => scrollToCat(catList[0])}>Tom</button>
        <button onClick={() => scrollToCat(catList[5])}>Maru</button>
        <button onClick={() => scrollToCat(catList[9])}>Jellylorum</button>
      </nav>
      <div>
        <ul>
          {catList.map((cat) => (
            <li
              key={cat}
              ref={(node) => {
                const map = getMap();
                if (node) {
                  map.set(cat, node);
                } else {
                  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;
}

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

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

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

Canary

Este exemplo mostra outra abordagem para gerenciar o Map com uma função de limpeza de callback ref.

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

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

Acessando os nós DOM de outro componente

Quando você coloca um ref em um componente embutido que gera um elemento de navegador como <input />, o React definirá a propriedade current desse ref para o nó DOM correspondente (como o <input /> real no navegador).

No entanto, se você tentar colocar um ref em seu próprio componente, como <MyInput />, por padrão você obterá null. Aqui está um exemplo demonstrando isso. Note como clicar no botão não foca a entrada:

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}>
        Focar na entrada
      </button>
    </>
  );
}

Para ajudá-lo a notar o problema, o React também imprime um erro no console:

Console
Aviso: Componentes de função não podem receber refs. Tentativas de acessar este ref falharão. Você quis dizer para usar React.forwardRef()?

Isso acontece porque, por padrão, o React não permite que um componente acesse os nós DOM de outros componentes. Nem mesmo para seus próprios filhos! Isso é intencional. Os refs são uma escapatória que deve ser usada com moderação. Manipular manualmente os nós DOM de outro componente torna seu código ainda mais frágil.

Em vez disso, componentes que querem expor seus nós DOM devem optar por esse comportamento. Um componente pode especificar que “encaminha” seu ref para um de seus filhos. Aqui está como MyInput pode usar a API forwardRef:

const MyInput = forwardRef((props, ref) => {
return <input {...props} ref={ref} />;
});

Assim funciona:

  1. <MyInput ref={inputRef} /> diz ao React para colocar o nó DOM correspondente em inputRef.current. No entanto, cabe ao componente MyInput optar por isso—por padrão, ele não faz.
  2. O componente MyInput é declarado usando forwardRef. Isso o opta para receber o inputRef de cima como o segundo argumento ref que é declarado após props.
  3. O MyInput em si passa o ref que recebeu para o <input> dentro dele.

Agora, clicar no botão para focar a entrada 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}>
        Focar na entrada
      </button>
    </>
  );
}

Em sistemas de design, é um padrão comum que componentes de baixo nível, como botões, entradas e assim por diante, encaminhem seus refs para seus nós DOM. Por outro lado, componentes de alto nível, como formulários, listas ou seções de página, geralmente não expõem seus nós DOM para evitar dependências acidentais na estrutura DOM.

Deep Dive

Expondo um subconjunto da API com uma handle imperativa

No exemplo acima, MyInput expõe o elemento DOM de entrada original. Isso permite que o componente pai chame focus() nele. No entanto, isso também permite que o componente pai faça algo mais—por exemplo, alterar seus estilos CSS. Em casos não comuns, você pode querer restringir a funcionalidade exposta. Você pode fazer isso com useImperativeHandle:

import {
  forwardRef, 
  useRef, 
  useImperativeHandle
} from 'react';

const MyInput = forwardRef((props, ref) => {
  const realInputRef = useRef(null);
  useImperativeHandle(ref, () => ({
    // Apenas expõe foco e mais nada
    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}>
        Focar na entrada
      </button>
    </>
  );
}

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

Quando o React anexa os refs

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

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

Em geral, você não quer acessar refs durante a renderização. Isso se aplica a refs que contêm nós DOM também. 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 das atualizações, os nós DOM ainda não foram atualizados. Portanto, é cedo demais para lê-los.

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

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

Deep Dive

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

Considere um código como este, que adiciona uma nova tarefa e rola a tela para o último filho da lista. Note como, por algum motivo, ele sempre rola para a tarefa que foi apenas antes da última adicionada:

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}>
        Adicionar
      </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: 'Tarefa #' + (i + 1)
  });
}

O problema está nessas duas linhas:

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

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

Para corrigir esse problema, você pode forçar o React a atualizar (“limpar”) o DOM de forma síncrona. Para fazer isso, importe flushSync de react-dom e envelope 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 na chamada flushSync. Como resultado, a última tarefa já estará no DOM no momento em que você tentar rolar para ela:

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}>
        Adicionar
      </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: 'Tarefa #' + (i + 1)
  });
}

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

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

Se você se limitar a ações não destrutivas, como focar e rolar, não deverá encontrar problemas. No entanto, se você tentar modificar o DOM manualmente, pode correr 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ê normalmente faria no React. O segundo botão usa a API DOM remove() para removê-lo à força do DOM fora do controle do React.

Tente pressionar “Alternar com setState” algumas vezes. A mensagem deve desaparecer e aparecer novamente. Em seguida, pressione “Remover do DOM”. Isso o removerá à força. Finalmente, pressione “Alternar com 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 com setState
      </button>
      <button
        onClick={() => {
          ref.current.remove();
        }}>
        Remover do DOM
      </button>
      {show && <p ref={ref}>Olá mundo</p>}
    </div>
  );
}

Depois de você remover manualmente o elemento DOM, tentar usar setState para mostrá-lo novamente resultará em uma falha. Isso acontece porque você mudou o DOM, e o React não sabe como continuar gerenciando-o corretamente.

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

No entanto, isso não significa que você não possa fazê-lo. Isso requer cautela. Você pode modificar partes do DOM que o React não tem razão para atualizar com segurança. Por exemplo, se algum <div> estiver sempre vazio no JSX, o React não terá razão 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ê os 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 forwardRef e passando o segundo argumento ref para baixo para um nó específico.
  • Evite mudar nós DOM gerenciados pelo React.
  • Se você modificar nós DOM gerenciados pelo React, modifique partes que o React não tenha razão para atualizar.

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

Neste exemplo, o botão alterna uma variável de estado para mudar entre um estado de reprodução e um estado pausado. 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 um 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 ? 'Pausar' : 'Reproduzir'}
      </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 “Reproduzir” em sincronia com se o vídeo está sendo reproduzido, mesmo que o usuário clique com o botão direito 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.