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 nadaDaí 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.