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

  1. Declare inputRef com o Hook useRef.
  2. Passe-o como <input ref={inputRef}>. Isso informa ao React para colocar o nó DOM deste <input> em inputRef.current.
  3. Na função handleClick, leia o nó DOM de entrada de inputRef.currente 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. 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

Como gerenciar uma lista de refs usando um ref callback

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.

Note

Quando o Modo Strict estiver ativado, callbacks de ref serão executados duas vezes durante o desenvolvimento.

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

Acessando os nós DOM de outro componente

Pitfall

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

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

Expondo um subconjunto da API com um manipulador imperativo

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

Despejando as atualizações de state de forma síncrona com flushSync

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