O que é useRef e por que vai além de useState
O useRef é um hook do React que retorna um objeto mutável cuja propriedade .current persiste entre renders. Diferente de useState, modificar um ref não dispara re-renders. Isso torna o hook perfeito para casos onde você precisa manter valores sem afetar a renderização ou acessar elementos do DOM diretamente.
A razão pela qual useRef é tão poderoso é que ele quebra o ciclo de dados unidirecional do React. Enquanto useState força a filosofia "estado muda → componente re-renderiza", useRef permite armazenar dados que podem ser lidos e escritos sem consequências de renderização. Isso é especialmente útil em cenários onde performance importa ou quando você precisa integrar com APIs imperativos do navegador.
Vamos pensar em um caso prático: um aplicativo que precisa focar um input quando um botão é clicado, ou um contador que incrementa internamente sem atualizar a UI. Esses são cenários perfeitos para useRef. Você verá que, apesar de simples em conceito, a aplicação correta do hook resolve problemas que seriam complicados com useState.
Acessando e Manipulando o DOM Diretamente
Referências a elementos DOM
A forma mais comum de usar useRef é obter uma referência direta a um elemento do DOM. Você anexa o ref ao atributo ref de um elemento JSX, e então acessa a instância real do DOM via .current.
import { useRef } from 'react';
function TextInputWithFocus() {
const inputRef = useRef(null);
const handleFocus = () => {
if (inputRef.current) {
inputRef.current.focus();
inputRef.current.style.borderColor = 'blue';
}
};
return (
<>
<input ref={inputRef} type="text" placeholder="Digite algo..." />
<button onClick={handleFocus}>Focar no input</button>
</>
);
}
export default TextInputWithFocus;
Aqui, inputRef.current aponta para o elemento <input> real do DOM. Quando você clica no botão, a função handleFocus chama o método .focus() diretamente no elemento, sem passar por nenhum sistema de estado. Isso é imperativo puro — você está dizendo ao navegador exatamente o que fazer.
Integrando com bibliotecas externas
Muitas bibliotecas JavaScript (como D3, Chart.js ou editor de código) funcionam de forma imperativa. Elas esperam um nó do DOM e controlam tudo internamente. useRef é a ponte perfeita entre o mundo declarativo do React e essas bibliotecas.
import { useRef, useEffect } from 'react';
import Chart from 'chart.js/auto';
function BarChart() {
const canvasRef = useRef(null);
const chartRef = useRef(null);
useEffect(() => {
if (canvasRef.current && !chartRef.current) {
chartRef.current = new Chart(canvasRef.current, {
type: 'bar',
data: {
labels: ['Jan', 'Fev', 'Mar', 'Abr'],
datasets: [{
label: 'Vendas',
data: [12, 19, 3, 5],
backgroundColor: 'rgba(75, 192, 192, 0.2)',
borderColor: 'rgba(75, 192, 192, 1)',
borderWidth: 1
}]
}
});
}
return () => {
if (chartRef.current) {
chartRef.current.destroy();
}
};
}, []);
return <canvas ref={canvasRef} width="400" height="200"></canvas>;
}
export default BarChart;
O padrão aqui é crucial: criamos a instância da biblioteca só uma vez (verificamos se já existe com !chartRef.current), armazenamos em um ref e limpamos quando o componente é desmontado. Assim evitamos múltiplas instâncias e vazamento de memória.
Valores Mutáveis e Persistência Entre Renders
Armazenando valores sem disparo de re-renders
Um uso avançado de useRef é manter valores que mudam, mas não precisam atualizar a interface. Imagine um timer que conta internamente enquanto você vê apenas o resultado final quando pressiona um botão. useRef permite isso sem criar renders desnecessários.
import { useRef, useState } from 'react';
function StopWatch() {
const [displayTime, setDisplayTime] = useState(0);
const timeRef = useRef(0);
const intervalRef = useRef(null);
const isRunningRef = useRef(false);
const handleStart = () => {
if (isRunningRef.current) return;
isRunningRef.current = true;
intervalRef.current = setInterval(() => {
timeRef.current += 1;
}, 1000);
};
const handleStop = () => {
isRunningRef.current = false;
clearInterval(intervalRef.current);
setDisplayTime(timeRef.current);
};
const handleReset = () => {
isRunningRef.current = false;
clearInterval(intervalRef.current);
timeRef.current = 0;
setDisplayTime(0);
};
return (
<div>
<p>Tempo: {displayTime}s</p>
<button onClick={handleStart}>Iniciar</button>
<button onClick={handleStop}>Parar e Exibir</button>
<button onClick={handleReset}>Resetar</button>
</div>
);
}
export default StopWatch;
Note que timeRef.current é incrementado 1000 vezes por segundo internamente, mas o componente não re-renderiza. Apenas quando você clica em "Parar e Exibir" é que setDisplayTime é chamado, disparando um render. Isso é extremamente eficiente: você evita 1000 re-renders desnecessários.
Tracking de valores anteriores
Um padrão avançado é usar useRef para rastrear o valor anterior de um estado. Isso é útil quando você precisa saber se algo mudou ou comparar valores entre renders.
import { useRef, useEffect, useState } from 'react';
function ComponenteComRastreamento() {
const [count, setCount] = useState(0);
const prevCountRef = useRef();
useEffect(() => {
prevCountRef.current = count;
}, [count]);
return (
<div>
<p>Agora: {count}</p>
<p>Antes: {prevCountRef.current}</p>
<p>Mudou: {count !== prevCountRef.current ? 'Sim' : 'Não'}</p>
<button onClick={() => setCount(count + 1)}>Incrementar</button>
</div>
);
}
export default ComponenteComRastreamento;
O useEffect executa após o render, então captura o valor antigo de count em prevCountRef.current. No próximo render, você consegue comparar o novo valor com o anterior. Esse padrão é base para implementar lógicas de "se mudou, faça algo".
Comunicação Entre Renders e Fluxos de Dados Complexos
Sincronizando múltiplos refs e estados
Em aplicações mais complexas, você pode precisar sincronizar vários refs e estados para manter a lógica coerente. Um exemplo real é um formulário com validação que precisa rastrear valores, erros e estados de envio sem re-renderizar desnecessariamente.
import { useRef, useState } from 'react';
function AdvancedForm() {
const [submitting, setSubmitting] = useState(false);
const [submitted, setSubmitted] = useState(false);
const formRef = useRef(null);
const validationStateRef = useRef({
name: true,
email: true,
isValid: false
});
const fieldValuesRef = useRef({
name: '',
email: ''
});
const validateField = (name, value) => {
let isValid = true;
if (name === 'name') {
isValid = value.trim().length >= 3;
} else if (name === 'email') {
isValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
}
validationStateRef.current[name] = isValid;
fieldValuesRef.current[name] = value;
// Atualizar validação geral
validationStateRef.current.isValid =
validationStateRef.current.name &&
validationStateRef.current.email;
};
const handleChange = (e) => {
const { name, value } = e.target;
validateField(name, value);
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!validationStateRef.current.isValid) {
alert('Formulário inválido');
return;
}
setSubmitting(true);
try {
// Simular chamada à API
await new Promise(resolve => setTimeout(resolve, 1000));
console.log('Dados enviados:', fieldValuesRef.current);
setSubmitted(true);
// Limpar após sucesso
setTimeout(() => {
setSubmitted(false);
formRef.current?.reset();
fieldValuesRef.current = { name: '', email: '' };
validationStateRef.current = {
name: true,
email: true,
isValid: false
};
}, 2000);
} finally {
setSubmitting(false);
}
};
return (
<form ref={formRef} onSubmit={handleSubmit}>
<div>
<input
type="text"
name="name"
placeholder="Nome"
onChange={handleChange}
/>
{!validationStateRef.current.name && <p style={{ color: 'red' }}>Nome deve ter 3+ caracteres</p>}
</div>
<div>
<input
type="email"
name="email"
placeholder="Email"
onChange={handleChange}
/>
{!validationStateRef.current.email && <p style={{ color: 'red' }}>Email inválido</p>}
</div>
<button
type="submit"
disabled={submitting || !validationStateRef.current.isValid}
>
{submitting ? 'Enviando...' : 'Enviar'}
</button>
{submitted && <p style={{ color: 'green' }}>Formulário enviado com sucesso!</p>}
</form>
);
}
export default AdvancedForm;
Aqui você vê múltiplos refs trabalhando juntos: validationStateRef rastreia a validade, fieldValuesRef armazena os valores do formulário, e formRef permite resetar o formulário. Nada disso dispara re-renders até que seja necessário (quando setSubmitting ou setSubmitted são chamados). Isso mantém o formulário responsivo mesmo com lógica complexa.
Coordenando fluxos assíncronos
Um cenário avançado é usar refs para coordenar múltiplas operações assíncronas sem que uma interfira na outra.
import { useRef, useState, useCallback } from 'react';
function DataFetcher() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const requestIdRef = useRef(null);
const activeRequestsRef = useRef(new Set());
const fetchData = useCallback(async (url) => {
const currentRequestId = Symbol('request');
requestIdRef.current = currentRequestId;
activeRequestsRef.current.add(currentRequestId);
setLoading(true);
try {
const response = await fetch(url);
const result = await response.json();
// Só atualiza o estado se este request ainda é o mais recente
if (requestIdRef.current === currentRequestId) {
setData(result);
}
} catch (error) {
if (requestIdRef.current === currentRequestId) {
setData({ error: error.message });
}
} finally {
activeRequestsRef.current.delete(currentRequestId);
if (activeRequestsRef.current.size === 0) {
setLoading(false);
}
}
}, []);
return (
<div>
<button onClick={() => fetchData('https://jsonplaceholder.typicode.com/posts/1')}>
Fetch Post 1
</button>
<button onClick={() => fetchData('https://jsonplaceholder.typicode.com/posts/2')}>
Fetch Post 2
</button>
{loading && <p>Carregando...</p>}
{data && (
<div>
<h3>{data.title || 'Erro ao carregar'}</h3>
<p>{data.body || data.error}</p>
</div>
)}
</div>
);
}
export default DataFetcher;
Este exemplo mostra race conditions resolvidas com refs. Quando você faz dois requests rapidamente, apenas o mais recente atualiza o estado. O ref requestIdRef rastreia qual é o request "vencedor" e activeRequestsRef monitora todas as requisições ativas. Sem isso, você enfrentaria bugs onde requests antigos sobrescrevem dados novos.
Conclusão
Aprendemos que useRef vai muito além de simplesmente acessar o DOM. Primeiro, é uma ferramenta para armazenar valores mutáveis que não disparam re-renders, permitindo otimizações drasticamente e facilitando integrações com APIs imperativos. Segundo, refs são excelentes para rastrear estado anterior, sincronizar múltiplos valores e resolver race conditions em operações assíncronas — tudo sem o overhead de renderização. Terceiro, o domínio de useRef avançado o coloca em um nível onde consegue escrever código React altamente eficiente e robusto, sabendo exatamente quando e por que usar refs em vez de estado.