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 HookuseRef
- 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:
- Declare
inputRef
com o HookuseRef
. - Passe-o como
<input ref={inputRef}>
. Isso diz ao React para colocar o nó do DOM deste<input>
eminputRef.current
. - Na função
handleClick
, leia o nó do DOM da entrada deinputRef.current
e chamefocus()
nele cominputRef.current.focus()
. - Passe o manipulador de eventos
handleClick
para<button>
comonClick
.
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
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.
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:
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:
<MyInput ref={inputRef} />
diz ao React para colocar o nó DOM correspondente eminputRef.current
. No entanto, cabe ao componenteMyInput
optar por isso—por padrão, ele não faz.- O componente
MyInput
é declarado usandoforwardRef
. Isso o opta para receber oinputRef
de cima como o segundo argumentoref
que é declarado apósprops
. - O
MyInput
em si passa oref
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
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
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 argumentoref
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.