useRef no React: acessar o DOM e guardar valor
Como pegar um elemento com ref, dar foco num input, guardar valor entre renders sem provocar renderização e a diferença para o useState.
A recepcionista clica em “Buscar”, mas o cursor não entra no campo. Depois, clica em “Parar” e o cronômetro continua correndo. Os dois bugs parecem diferentes, mas nascem da mesma dúvida: onde guardar algo que precisa sobreviver entre execuções do componente sem aparecer na tela?
React é uma biblioteca JavaScript para criar interfaces. Um componente é
uma função que descreve uma parte da tela em JSX, marcação parecida com HTML;
cada execução dessa função é um render. Estado é a memória que avisa ao
React quando a interface precisa ser redesenhada. Um hook é uma função do
React, como useRef e useState, que adiciona recursos ao componente.
useRef cria uma ref, uma referência persistente cujo valor fica em
current. Ela pode apontar para um elemento do DOM — a representação da
página no navegador — ou guardar um dado de bastidor. Nesta lição você vai
escolher entre ref e estado, focar um campo, controlar um timer e diagnosticar o
erro de current ainda vazio.
Os exemplos formam uma só aplicação: a recepção de uma clínica veterinária, com a busca de paciente, a ficha do Rex e o cronômetro da consulta.
Para que serve useRef no React?
Use useRef para guardar um nó do DOM — o objeto que representa um elemento da
página — ou um valor auxiliar que atravessa renders sem redesenhar a tela. Se a
pessoa precisa enxergar a mudança, esse dado deve ficar no estado.
Pense no componente como um balcão. O que o cliente precisa ver fica sobre ele
e deve atualizar a placa da tela; isso é estado. Algumas coisas precisam apenas
continuar guardadas nos bastidores — o identificador de um timer, o valor
anterior, a referência para o campo de busca. useRef cria essa gaveta
persistente. O objeto ref é a gaveta, e current é seu único compartimento.
A analogia ajuda a decidir onde guardar o dado, mas termina aí: uma gaveta real
não conhece montagem, render nem a pureza do React — calcular a tela sem
alterar dados externos. No código, trocar current não avisa a tela, e ler ou
escrever essa propriedade durante o render não é permitido, salvo uma
inicialização previsível mostrada mais adiante.
O que useRef devolve e quando current recebe o elemento do DOM?
useRef(null) devolve um objeto como { current: null }. Quando você o entrega
a um elemento pelo atributo ref, o React preenche current após a montagem — a
inserção no DOM. Durante o primeiro render, null representa ausência de valor.
import { useRef } from 'react';
export default function BuscaPaciente() {
const campo = useRef(null);
console.log('durante o render:', campo);
function focarCampo() {
console.log('no clique :', campo.current.tagName, '/', campo.current.placeholder);
campo.current.focus();
}
return (
<form>
<input ref={campo} placeholder="Nome do paciente" />
<button type="button" onClick={focarCampo}>
Buscar
</button>
</form>
);
}Experimente você mesmo
Antes de executar, preveja o conteúdo de campo.current durante o render e no
clique. Depois monte o componente, clique em “Buscar” e confirme também se o
campo recebe foco.
Três informações nessa saída. Primeira: durante o render, current ainda vale
null — o <input> ainda nem existe na página. Segunda: no clique, current já
é o elemento de verdade, com tagName e placeholder. Terceira, a linha que o
teste imprimiu comparando document.activeElement com o input: focus()
funcionou, e o cursor está piscando dentro do campo.
O console.log durante o render existe apenas para revelar a ordem do teste.
Não use leitura de ref.current no corpo do componente como padrão de produção;
leia e escreva refs em eventos — interações como clique — ou efeitos, tarefas
executadas depois que o React sincroniza a tela. A exceção é a inicialização
pura e previsível com current === null, explicada adiante.
Quem passa a ref para o React é você, no atributo ref={campo}. Quem preenche
o current é o React, depois de colocar o elemento na tela. É a mesma ideia do
querySelector e getElementById,
com uma diferença que importa: querySelector procura no documento inteiro e
pode achar o input de outro card da lista; o ref entrega o nó daquela
instância do componente, sem precisar inventar um id único.
Até aqui: o React preenche a ref depois da montagem; por isso o clique já
encontra o input, enquanto o primeiro render ainda encontra null.
Alterar ref.current causa um novo render no React?
Não. Alterar current preserva o valor, mas não agenda outro render. Se essa
mudança precisa aparecer na interface, use estado; o comparativo abaixo torna a
diferença observável.
São duas fichas de vacinação idênticas: uma guarda a contagem numa ref, a outra no estado. Ambas também contam quantas vezes o componente executou.
import { useRef } from 'react';
export default function FichaComRef() {
const vacinas = useRef(0);
const renders = useRef(0);
renders.current += 1;
function aplicar() {
vacinas.current += 1;
}
function conferir() {
console.log('ref → vacinas:', vacinas.current, '| renders:', renders.current);
}
return (
<div>
<p>Vacinas aplicadas: {vacinas.current}</p>
<button id="aplicar" onClick={aplicar}>Aplicar vacina</button>
<button id="conferir" onClick={conferir}>Conferir no console</button>
</div>
);
}import { useRef, useState } from 'react';
export default function FichaComState() {
const [vacinas, setVacinas] = useState(0);
const renders = useRef(0);
renders.current += 1;
function aplicar() {
setVacinas((anterior) => anterior + 1);
}
function conferir() {
console.log('state → vacinas:', vacinas, '| renders:', renders.current);
}
return (
<div>
<p>Vacinas aplicadas: {vacinas}</p>
<button id="aplicar" onClick={aplicar}>Aplicar vacina</button>
<button id="conferir" onClick={conferir}>Conferir no console</button>
</div>
);
}Experimente você mesmo
Antes de clicar, preveja quatro valores após 50 aplicações: contagem guardada, texto na tela e número de renders em cada ficha. Depois faça os 50 cliques e use “Conferir no console” para comparar sua previsão com a saída.
Leia devagar, porque tem uma armadilha aí. As duas contagens chegaram a 50 — o
ref guardou o valor tão bem quanto o estado. Mas a ficha com ref rodou um
render, e a tela dela continua escrita “Vacinas aplicadas: 0”. O <p> foi
desenhado uma vez, quando vacinas.current valia zero, e nunca mais.
A ficha com estado rodou 51 renders — o inicial mais um por clique — e a tela acompanhou.
Há instrumentação didática nos dois blocos: renders.current += 1 muda uma ref
durante o render; no primeiro, o JSX também lê vacinas.current. Isso expõe a
diferença no teste, mas não é padrão de produção. O render deve ser puro; em
código real, acesse a ref em eventos e efeitos, e use estado para o que aparece.
As duas últimas linhas da saída são o tempo dos 50 cliques, num MacBook com Node
24.16.0, média de 5 execuções descartando o aquecimento. O número exato oscila
entre execuções — o useState ficou entre 3,1 ms e 6 ms nas medições, o useRef
sempre abaixo de 2 ms. O que não oscila é a contagem de renders: 1 e 51, em
toda execução.
Daí sai a regra prática: se o valor aparece na tela, ele é estado. Ref é para o que fica nos bastidores. E se você quiser entender o que exatamente faz o React rodar um componente de novo, é o assunto de re-render no React.
Até aqui: ref e estado preservam valores, mas só a atualização de estado avisa ao React que a interface precisa ser calculada novamente.
O valor passado ao useRef é calculado em todo render?
Sim: o JavaScript calcula a expressão passada a useRef a cada execução do
componente, embora o React guarde apenas o primeiro resultado. Para construir
um objeto puro e caro uma só vez, inicialize current sob uma condição segura.
A ficha abaixo guarda o número de consultas no estado e, ao lado, o primeiro número recebido pela ref:
import { useRef, useState } from 'react';
let leituras = 0;
function lerTabelaDeVacinas() {
leituras += 1;
return { antirrabica: 90, v10: 45, giardia: 60 };
}
export default function Ficha() {
const [consultas, setConsultas] = useState(0);
const tabela = useRef(lerTabelaDeVacinas());
const primeiroValor = useRef(consultas);
return (
<div>
<p>
Consultas: {consultas} — primeiro valor guardado: {primeiroValor.current} —
antirrábica em {tabela.current.antirrabica} dias
</p>
<button onClick={() => setConsultas((n) => n + 1)}>Registrar consulta</button>
</div>
);
}Três cliques em “Registrar consulta”:
primeiroValor.current ficou em 0 enquanto consultas subiu para 3. Isso é
útil de propósito — é assim que se guarda “o valor que era” — mas pega gente
desprevenida que esperava um espelho do estado.
O contador global leituras e as leituras de current no JSX são
instrumentação para tornar o comportamento visível. Não copie essa parte para
dados de interface: se o valor precisa aparecer, mantenha-o no estado.
A última linha é a parte cara. lerTabelaDeVacinas() rodou quatro vezes: uma na
montagem e uma em cada um dos três renders que os cliques provocaram. O React só
usou o resultado da primeira, e as outras três foram jogadas fora — o JavaScript
precisa executar o argumento antes de passar. Diferente do useState, o
useRef não tem forma preguiçosa para você entregar a função em vez do valor.
O jeito de contornar é inicializar dentro de um if:
import { useRef, useState } from 'react';
let leituras = 0;
function lerTabelaDeVacinas() {
leituras += 1;
return { antirrabica: 90, v10: 45, giardia: 60 };
}
export default function Ficha() {
const [consultas, setConsultas] = useState(0);
const tabela = useRef(null);
if (tabela.current === null) {
tabela.current = lerTabelaDeVacinas();
}
return (
<div>
<p>
Consultas: {consultas} — antirrábica em {tabela.current.antirrabica} dias
</p>
<button onClick={() => setConsultas((n) => n + 1)}>Registrar consulta</button>
</div>
);
}Uma leitura só. O leituras += 1 continua sendo apenas o contador do teste; a
operação real deve ser pura. Esse if (current === null) é a exceção permitida
durante o render quando sempre produz o mesmo objeto previsível. Vale para
montar um índice ou instanciar uma classe sem efeitos colaterais. Para
useRef(0) ou useRef(null), use a forma direta.
Como guardar o valor anterior com useRef?
Uma ref pode registrar o último valor dentro de um efeito para ser consultado
depois por outro efeito ou evento. Se o valor anterior participa do JSX, modele
um estado que guarde os dois pesos; ler current no render não é seguro.
A balança da clínica registra o peso do Rex a cada consulta, e a tela precisa mostrar quanto ele variou desde a pesagem anterior. O bloco abaixo torna a ordem temporal observável: a ref recebe o peso dentro de um efeito, função executada depois que a tela foi desenhada.
Ele é uma demonstração da mecânica, não um modelo para copiar quando a comparação precisa aparecer na interface:
import { useEffect, useRef, useState } from 'react';
const PESAGENS = [8.2, 7.6, 7.9, 9.4];
export default function Balanca() {
const [indice, setIndice] = useState(0);
const peso = PESAGENS[indice];
const pesoAnterior = useRef(null);
useEffect(() => {
pesoAnterior.current = peso;
}, [peso]);
const anterior = pesoAnterior.current;
return (
<div>
<p>
Rex: {peso} kg — anterior:{' '}
{anterior === null
? 'primeira pesagem'
: `${anterior} kg (${peso > anterior ? '+' : ''}${(peso - anterior).toFixed(1)} kg)`}
</p>
<button onClick={() => setIndice((i) => i + 1)}>Registrar próxima pesagem</button>
</div>
);
}A ordem é o segredo. Quando o peso vira 7,6, o componente roda com
pesoAnterior.current ainda valendo 8,2 — a tela é desenhada com a comparação
certa — e só depois o useEffect grava o novo valor,
sem disparar render nenhum.
Esse resultado explica o tempo do efeito, mas o exemplo lê
pesoAnterior.current durante o render. Pela regra de pureza do React, use a
ref para comparações feitas em efeitos ou eventos; se a comparação é visível,
guarde o peso atual e o anterior juntos no estado.
Também não basta trocar a ref por um useState gravado no mesmo efeito. Nesse
arranjo específico, as quatro pesagens levam o contador de renders a 2, 4, 6 e
8. No segundo render o anterior já foi sobrescrito pelo peso atual, e a última
linha estaciona em
anterior: 9.4 kg (0.0 kg). A variação some para sempre. O ref não sofre disso
porque gravar nele não provoca o segundo render.
O ponto é separar duas necessidades: bastidor consultado fora do render pode usar ref; histórico que compõe a tela pertence ao modelo de estado.
Como guardar o id de setInterval entre renders?
Guarde o identificador do setInterval numa ref. Uma variável local volta ao
valor inicial em cada render; a ref conserva o identificador para que
clearInterval encontre e pare a repetição.
O cronômetro da consulta é o caso em que a diferença entre variável comum e ref
vira um bug observável. setInterval agenda uma função repetida e devolve um
id, o identificador desse agendamento. A versão ingênua o guarda numa
variável local:
import { useState } from 'react';
export default function CronometroQuebrado() {
const [segundos, setSegundos] = useState(0);
let idDoTimer = null;
function iniciar() {
idDoTimer = setInterval(() => setSegundos((s) => s + 1), 1000);
}
function parar() {
clearInterval(idDoTimer);
}
return (
<div>
<p>Consulta do Rex: {segundos}s</p>
<button id="iniciar" onClick={iniciar}>Iniciar</button>
<button id="parar" onClick={parar}>Parar</button>
</div>
);
}Trocando let idDoTimer = null por um ref, e nada mais:
import { useRef, useState } from 'react';
export default function Cronometro() {
const [segundos, setSegundos] = useState(0);
const idDoTimer = useRef(null);
function iniciar() {
if (idDoTimer.current !== null) return;
idDoTimer.current = setInterval(() => setSegundos((s) => s + 1), 1000);
}
function parar() {
clearInterval(idDoTimer.current);
idDoTimer.current = null;
}
return (
<div>
<p>Consulta do Rex: {segundos}s</p>
<button id="iniciar" onClick={iniciar}>Iniciar</button>
<button id="parar" onClick={parar}>Parar</button>
</div>
);
}O teste foi o mesmo nos dois: clicar em “Iniciar”, esperar 3 segundos de relógio, clicar em “Parar” e esperar mais 3 segundos.
O cronômetro da primeira versão continuou correndo depois do “Parar” e chegou a
6 segundos. O motivo: cada tick do intervalo chama setSegundos, que provoca um
render, e cada render executa let idDoTimer = null de novo. Quando você clica
em “Parar”, o botão está ligado ao parar do último render — aquele em que
idDoTimer é null. E clearInterval(null) não faz nada, sem reclamar.
Na versão corrigida, a condição evita iniciar dois intervalos. iniciar guarda
o id em current; parar entrega esse id a clearInterval e volta a ref para
null, deixando o cronômetro pronto para um novo início.
O ref não sofre disso porque é sempre o mesmo objeto: o React devolve a mesma
caixa em todo render, e idDoTimer.current continua apontando para o id que foi
guardado lá atrás. Se você ainda não viu como setInterval e clearInterval
funcionam por fora do React, a lição de
setTimeout e setInterval cobre isso.
Até aqui: variável local recomeça em cada render; a mesma ref atravessa todos eles e permite cancelar o timer certo.
Quando usar useRef ou useState?
Use estado com useState quando uma mudança precisa aparecer na tela. Use ref quando apenas eventos, efeitos ou uma API — recurso oferecido pelo navegador, como foco ou timer — precisa do valor. Velocidade não é o critério principal.
Na tabela, AbortController é um objeto do navegador usado para cancelar uma
operação assíncrona, aquela que termina depois; flag é um sinal interno,
geralmente true ou false.
| o que você quer guardar | hook | por quê |
|---|---|---|
| quantidade, texto, item selecionado — qualquer coisa que aparece na tela | useState |
a tela só atualiza quando o React é avisado |
| o elemento do DOM para focar, medir ou rolar | useRef |
o nó não é dado de tela, é ferramenta |
id de setInterval, setTimeout ou de um AbortController |
useRef |
é bastidor puro; mudar não precisa redesenhar nada |
| o valor anterior de um estado, para comparar | useRef |
guardar o antigo não pode provocar um render novo |
| se um efeito já rodou uma vez | useRef |
flag de controle; a tela não muda por causa dela |
| valor calculado a partir de outro estado | nenhum dos dois | calcule direto no corpo do componente |
| dado que vem do servidor e é exibido | useState |
precisa redesenhar quando chega |
A linha do valor anterior exige o limite visto acima: ref funciona quando a comparação acontece num efeito ou evento. Se o histórico compõe o JSX, ele precisa fazer parte do estado da interface.
Como passar ref para um componente no React 19?
No React 19, receba ref como uma prop e repasse-a ao elemento HTML. Prop é
uma configuração entregue ao componente; se ele ignorar essa prop, a ref do pai
continua null. No React 18, essa passagem exige forwardRef.
Compare os dois campos de busca: o primeiro ignora a ref, o segundo a entrega ao
<input>.
function CampoBuscaSurdo({ etiqueta }) {
return (
<label>
{etiqueta}
<input placeholder="Nome do paciente" />
</label>
);
}
function CampoBusca({ etiqueta, ref }) {
return (
<label>
{etiqueta}
<input ref={ref} placeholder="Nome do paciente" />
</label>
);
}A recepção usa um ou outro, sem mudar mais nada:
import { useRef } from 'react';
export default function Recepcao({ Campo, rotulo }) {
const campo = useRef(null);
function focar() {
console.log(rotulo, '→ campo.current:', campo.current === null ? 'null' : campo.current.tagName);
campo.current?.focus();
}
return (
<div>
<Campo etiqueta="Paciente" ref={campo} />
<button onClick={focar}>Focar a busca</button>
</div>
);
}Sem repassar, o current do pai fica null para sempre e nenhum aviso aparece
no console — o React 19 aceita ref como prop qualquer, e uma prop que o
componente não usa é simplesmente ignorada. O ?. é optional chaining: ele
só chama focus() se current existir. Isso evita o erro, mas não corrige o
repasse; o botão continua sem focar o campo.
Por que ref.current ainda está null?
current fica null antes da montagem ou enquanto um elemento condicional
ainda não existe. Espere um evento ou efeito após a montagem, ou use uma ref de
callback para agir quando o próprio React entregar o nó.
No primeiro erro, a chamada de foco está no corpo do componente:
import { useRef } from 'react';
export default function BuscaPaciente() {
const campo = useRef(null);
campo.current.focus();
return <input ref={campo} placeholder="Nome do paciente" />;
}TypeError: Cannot read properties of null (reading ‘focus’) at BuscaPaciente (file:///private/tmp/pet-clinica/r5.mjs:8:17) at Object.react_stack_bottom_frame (/private/tmp/pet-clinica/node_modules/react-dom/cjs/react-dom-client.development.js:25904:20) at renderWithHooks (/private/tmp/pet-clinica/node_modules/react-dom/cjs/react-dom-client.development.js:7662:22) at updateFunctionComponent (/private/tmp/pet-clinica/node_modules/react-dom/cjs/react-dom-client.development.js:10166:19) at beginWork (/private/tmp/pet-clinica/node_modules/react-dom/cjs/react-dom-client.development.js:11778:18)
Node.js v24.16.0
O render acontece antes de o elemento existir. É o React executando a sua
função para descobrir o que desenhar; nesse instante não há input nenhum no
documento, e current é o null que você mesmo passou.
A segunda forma do mesmo erro é mais sutil, porque o clique parece um lugar seguro. O campo só existe depois que a recepcionista abre a consulta:
import { useRef, useState } from 'react';
export default function Recepcao() {
const [aberto, setAberto] = useState(false);
const campo = useRef(null);
function abrirEFocar() {
setAberto(true);
campo.current.focus();
}
return (
<div>
<button onClick={abrirEFocar}>Nova consulta</button>
{aberto && <input ref={campo} placeholder="Nome do paciente" />}
</div>
);
}setAberto(true) não desenha o input na linha seguinte: ele agenda um render. Na
linha do focus() o campo ainda não nasceu, e current continua null.
A correção mais direta é uma função no lugar do objeto. Quando você passa uma
função para ref, você cria uma ref de callback. O React chama essa função
com o nó assim que ele entra na tela e, no React 19, usa o retorno como função de
limpeza quando o elemento sai:
import { useState } from 'react';
export default function Recepcao() {
const [aberto, setAberto] = useState(false);
function guardarCampo(no) {
console.log('campo montado :', no.tagName);
no.focus();
return () => console.log('campo desmontado');
}
return (
<div>
<button id="abrir" onClick={() => setAberto(true)}>Nova consulta</button>
<button id="fechar" onClick={() => setAberto(false)}>Cancelar</button>
{aberto && <input ref={guardarCampo} placeholder="Nome do paciente" />}
</div>
);
}Clicando em “Nova consulta” e depois em “Cancelar”:
O foco entrou no momento certo, sem useRef nenhum, porque quem avisa que o
elemento chegou é o próprio React. Quando o assunto é foco automático na
montagem, um useEffect com a chamada dentro resolve igual — o que não resolve
é chamar focus() no meio do render ou logo depois de pedir uma atualização de
estado.
O que estudar depois de useRef no React?
Na ordem 14 da trilha, estude useMemo e useCallback. Eles guardam resultado de cálculo ou identidade de função para otimização; não substituem ref nem estado.
A trilha de React mostra a sequência completa, e o guia de React organiza os hooks por assunto.
Perguntas frequentes
Posso ler ref.current durante o render?
useRef substitui o querySelector dentro de um componente React?
Quantos useRef posso ter no mesmo componente?
forwardRef parou de funcionar no React 19?
Dá para colocar um ref em qualquer componente?
Dúvidas e comentários
Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.
Entrar para perguntarÉ o mesmo login gratuito dos cursos.
Nenhuma dúvida por aqui ainda — a primeira pode ser a sua.
Todo o código deste artigo foi executado em Node 24.16.0, e as saídas exibidas são as reais — como produzimos este conteúdo.
Fontes consultadas
- React — useRef — react.dev
- React — Manipulating the DOM with Refs — react.dev
- React 19 — ref as a prop — react.dev


