Manipulando o DOM com Refs
O React atualiza automaticamente o DOM para corresponder à sua saída de renderização, então seus componentes geralmente não precisarão manipulá-lo. 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 um ref para o nó do DOM.
Você aprenderá
- Como acessar um nó DOM gerenciado pelo React com o atributo
ref
- Como o atributo JSX
ref
se relaciona com o HookuseRef
- Como acessar o nó DOM de outro componente
- Em quais casos é seguro modificar o DOM gerenciado pelo React
Obtendo um ref para o nó
Para acessar um nó DOM gerenciada 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 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 cria 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 de seus manipuladores de eventos e usar as APIs do navegador incorporadas definidas nele.
// Você pode usar qualquer API do navegador, por exemplo:
myRef.current.scrollIntoView();
Exemplo: Focando uma entrada de texto
Neste exemplo, clicar no botão 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}> Focus the input </button> </> ); }
Para implementar isso:
- Declare
inputRef
com o HookuseRef
. - Passe-o como
<input ref={inputRef}>
. Isso informa ao React para colocar o nó DOM deste<input>
eminputRef.current
. - Na função
handleClick
, leia o nó DOM de 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. Semelhante ao state, os refs permanecem entre as renderizações. Refs são como variáveis de state 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 único 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}> 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
Nos exemplos acima, existe um número predefinido de refs. No entanto, às vezes você pode precisar de um ref para cada item na lista e não saberá quantos terá. Algo como isso não funcionaria:
<ul>
{items.map((item) => {
// Não funciona!
const ref = useRef(null);
return <li ref={ref} />;
})}
</ul>
Isso ocorre porque os Hooks só devem 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 um único 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 sua estrutura DOM mudar.
Outra solução é passar uma função para o atributo ref
. Isso é chamado de ref
callback. O React chamará seu ref callback 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) { // Inicialize 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[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; }
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 ref
callback em cada item da lista se encarrega de atualizar o Map:
<li
key={cat.id}
ref={node => {
const map = getMap();
// Adicionar ao Map
map.set(cat, node);
return () => {
// Remover do Map
map.delete(cat);
};
}}
>
Isso permite que você leia nós DOM individuais do Map posteriormente.
Acessando os nós DOM de outro componente
Você pode passar refs do componente pai para os 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, um ref é criado no componente pai, MyForm
, e é passado para o componente filho, MyInput
. MyInput
então passa o ref para <input>
. Como <input>
é um componente integrado, o React define a propriedade .current
do ref para o elemento DOM <input>
.
O inputRef
criado em MyForm
agora aponta para o elemento DOM <input>
retornado por MyInput
. Um manipulador de cliques criado em MyForm
pode acessar inputRef
e chamar focus()
para definir o foco 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
No exemplo acima, o ref passado para MyInput
é repassado para o elemento de entrada 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, () => ({ // Expor 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 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. Portanto, inputRef.current
dentro do componente Form
terá apenas o método focus
. Neste caso, o “handle” do ref não é o nó DOM, mas o objeto personalizado 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 commit, o React aplica 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 das atualizações, os nós DOM ainda não foram atualizados. Então, é 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.
Normalmente, você acessará refs de manipuladores de eventos. Se você quiser fazer alguma coisa com um ref, mas não houver um evento específico para fazê-lo, pode precisar de um Effect. Discutiremos Effects nas próximas páginas.
Deep Dive
Considere um código como este, que adiciona um novo todo e rola a tela para baixo até o último filho da lista. Observe como, por alguma razão, ele sempre rola para o todo 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á com estas duas linhas:
setTodos([ ...todos, newTodo]);
listRef.current.lastChild.scrollIntoView();
No React, as atualizações de state são enfileiradas. Normalmente, é isso que você quer. No entanto, aqui ele causa um problema porque setTodos
não atualiza imediatamente o DOM. Assim, quando você rola a lista para seu último elemento, o todo ainda não foi adicionado. É por isso que a rolagem sempre “fica para trás” por um item.
Para corrigir este problema, você pode forçar o React a atualizar (“despejar”) o DOM de forma síncrona. Para fazer isso, importe flushSync
de react-dom
e encapsule a atualização do estado em uma chamada flushSync
:
flushSync(() => {
setTodos([ ...todos, newTodo]);
});
listRef.current.lastChild.scrollIntoView();
Isto irá instruir o React a atualizar o DOM de forma síncrona logo após a execução do código encapsulado em flushSync
. Como resultado, o último todo já fará parte do DOM no momento em que 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 saída de emergência. Você só deve usá-los quando precisar “sair do React”. Exemplos comuns disso incluem o gerenciamento de foco, a posição da rolagem ou a chamada de APIs do navegador que o React não expõe.
Se você se ater a ações não destrutivas, como foco e rolagem, não deve encontrar nenhum problema. No entanto, se você tentar modificar o DOM manualmente, poderá correr o risco de entrar em conflito 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 remove()
DOM API 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); }}> 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 uma falha. 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 falhas como acima.
No entanto, isso não significa que você não pode fazê-lo. Requer cautela. Você pode modificar com segurança partes do DOM que o React não tem motivos para atualizar. Por exemplo, se algum <div>
estiver sempre vazio no JSX, o React não terá motivos para tocar em sua lista de filhos. Portanto, é seguro adicionar ou remover elementos manualmente ali.
Recap
- Refs são um conceito genérico, mas na maioria das vezes você os usará para conter elementos DOM.
- Você instrui o React a colocar um nó do DOM em
myRef.current
passando<div ref={myRef}>
. - Normalmente, você usará refs para ações não destrutivas como foco, rolagem ou medição de 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 alterar nós DOM gerenciados pelo React.
- Se você modificar nós DOM gerenciados pelo React, modifique as partes que o React não tem motivos 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 um estado de 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 do <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 “Reproduzir” sincronizado com a reprodução do vídeo, 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 ao navegador. Talvez você queira ouvir onPlay
e onPause
no vídeo para fazer isso.