FNSP Web Page

Refs no React: a caixa que não provoca renders

O useRef costuma ser apresentado como «a forma de chegar a um elemento do DOM». É um dos usos, e provavelmente nem é o mais frequente. O que um ref é, na verdade, é mais simples do que isso e explica tudo o resto: uma caixa que sobrevive aos renders e cuja alteração não provoca nenhum.

1. O estado desenha; o ref não

const [contagem, setContagem] = useState(0);  // muda -> o React desenha
const contagemRef = useRef(0);                // muda -> não acontece nada

Daí sai a regra que decide entre os dois, e que não falha: se o ecrã tem de mudar quando o valor muda, é estado; se não tem, é um ref.

O identificador de um setTimeout, guardado para o poder cancelar mais tarde? Ninguém o vê. Ref. O número de tentativas falhadas que aparece por baixo do formulário? Vê-se. Estado.

2. Não leia nem escreva durante o render

// Não
function Componente() {
  const renders = useRef(0);
  renders.current++;              // escrita durante o render
  return <p>{renders.current}</p>;
}

// Sim: nos efeitos e nos handlers
useEffect(() => {
  renders.current++;
});

Não é pedantismo. O React reserva-se o direito de começar um render, deitá-lo fora e recomeçar — é o que o StrictMode faz de propósito em desenvolvimento. Um render que altera coisas fora dele deixa de dar o mesmo resultado quando é repetido, e o que aparece no ecrã passa a depender de quantas vezes o React decidiu tentar.

3. Refs para o DOM: vazios no primeiro render

function Campo() {
  const campo = useRef(null);

  useEffect(() => {
    campo.current.focus();   // aqui já existe
  }, []);

  return <input ref={campo} />;
}

A ordem nunca muda: o React desenha — e no primeiro render o campo.current ainda é null —, põe o resultado no DOM, preenche os refs, e só então corre os efeitos. Ler o .current no corpo do componente é ler cedo de mais.

4. Um ref pode ser uma função, e é aí que fica interessante

function Cabecalho() {
  const [altura, setAltura] = useState(0);

  return (
    <div
      ref={(no) => {
        if (no) setAltura(no.getBoundingClientRect().height);
      }}
    >
      ...
    </div>
  );
}

Uma função no ref é chamada com o nó no momento em que ele entra no DOM. Para medir alguma coisa, isto é melhor do que um useEffect com lista de dependências vazia: o efeito corre uma vez, na montagem, mas a função corre também quando o nó é substituído — o que acontece sem o componente desmontar, por exemplo quando uma condição troca a árvore por baixo dele.

No React 19 essa função pode devolver uma limpeza, que é a forma certa de desligar um ResizeObserver ou um addEventListener posto ali. Isso e o fim do forwardRef estão no artigo sobre a migração do 18 para o 19.

5. Vários refs, numa lista

Um useRef por item não é possível: os hooks não podem estar dentro de um ciclo. A saída é um Map guardado num ref só.

const linhas = useRef(new Map());

items.map((item) => (
  <li
    key={item.id}
    ref={(no) => {
      if (no) linhas.current.set(item.id, no);
      else linhas.current.delete(item.id);
    }}
  />
));

O delete não é enfeite: sem ele fica-se com um mapa a crescer, cheio de nós que já não estão em lado nenhum e que o browser não consegue libertar.

6. Expor um verbo, e não o nó

function Campo({ ref }) {
  const interno = useRef(null);

  useImperativeHandle(ref, () => ({
    focar: () => interno.current.focus(),
  }), []);

  return <input ref={interno} />;
}

Sem isto, quem usa o componente recebe o input inteiro e pode fazer-lhe o que quiser: mudar o valor, o estilo, as classes. Com isto recebe uma porta — campo.current.focar() — e mais nada. É menos poder, e é precisamente essa a intenção.

7. Quando o DOM tem de estar pronto já

function adicionar() {
  flushSync(() => {
    setLinhas([...linhas, nova]);   // desenha agora, e não mais tarde
  });

  lista.current.lastChild.scrollIntoView();
}

Por omissão o React junta várias alterações de estado e desenha uma vez só, depois. Se a linha seguinte precisa do DOM já atualizado — para medir, ou para deslocar o scroll até ao que acabou de nascer —, o flushSync força o desenho ali mesmo. É caro: usa-se quando não há alternativa, não por hábito.

O resumo

Um ref é uma caixa fora do ciclo de render. Serve para aquilo que o ecrã não mostra e para chegar ao DOM; escreve-se nele em efeitos e em handlers; e lê-se sabendo que no primeiro render ainda está vazio.

E quando parecer que só um ref resolve um problema de estado, vale a pena parar e perguntar se aquele valor não devia mesmo estar a provocar um render.

Comentários

Ainda ninguém comentou este artigo.

Voltar ao blog

Gostávamos de saber quantas pessoas visitam o site, com o Google Analytics. Sem a sua autorização não corre nada. Política de Privacidade.