Introdução: Por que os Hooks mudaram o React?
Desde que chegaram ao React na versão 16.8, no início de 2019, os Hooks se tornaram o padrão de facto da comunidade — praticamente toda biblioteca e tutorial moderno parte deles hoje.
Se você começou a programar em React antes da versão 16.8, provavelmente ainda tem na memória a saga dos class components: componentDidMount, componentWillUnmount, this.setState, e a eterna luta contra o binding de métodos. Foi um período de produtividade limitada e, convenhamos, de código verboso.
Então, os Hooks chegaram como uma revolução silenciosa. Eles trouxeram duas promessas centrais:
- Composição simples de lógica de estado – nada de hierarquias de herança ou “wrapper components” complicados.
- Acesso direto ao ciclo de vida – tudo dentro da mesma função, sem precisar criar classes.
Mas, como toda ferramenta poderosa, os Hooks exigem compreensão profunda. Não basta usar useState e useEffect de forma superficial; para realmente extrair o potencial do React, precisamos dominar os hooks avançados: useEffect com limpeza correta, useRef para valores persistentes e referências ao DOM, e, claro, criar nossos próprios custom hooks reutilizáveis.
Neste artigo, vamos mergulhar nos detalhes que fazem a diferença no dia‑a‑dia de um desenvolvedor full‑stack. Prepare o teclado, abra o VS Code e acompanhe os exemplos reais que você poderá copiar‑colar nos seus projetos.
useEffect com cleanup e dependências
O que o useEffect realmente faz?
O useEffect substitui os métodos de ciclo de vida das classes (componentDidMount, componentDidUpdate, componentWillUnmount). Ele aceita duas coisas:
useEffect(() => {
// efeito colateral (side‑effect)
return () => {
// função de limpeza (cleanup)
};
}, [/* lista de dependências */]);
- O primeiro argumento é a função que será executada após a renderização.
- O valor retornado (se houver) será chamado antes da próxima execução do efeito ou quando o componente for desmontado.
- O segundo argumento (array de dependências) controla quando o efeito deve ser re‑executado.
Exemplo 1 – Fetch de dados com cancelamento
Imagine um componente que busca detalhes de um usuário ao montar. Se o usuário navegar para outra página antes da requisição terminar, precisamos abortar a chamada para evitar memory leaks e atualizações de estado em componentes desmontados.
import { useEffect, useState } from 'react';
function useUser(id: string) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const controller = new AbortController(); // <-- permite cancelar
async function fetchUser() {
try {
const response = await fetch(`/api/users/${id}`, {
signal: controller.signal,
});
if (!response.ok) throw new Error('Falha ao buscar usuário');
const data = await response.json();
setUser(data);
} catch (e: any) {
if (e.name !== 'AbortError') setError(e.message);
} finally {
setLoading(false);
}
}
fetchUser();
// cleanup: aborta a requisição se o componente desmontar ou id mudar
return () => controller.abort();
}, [id]); // <-- dependência correta
return { user, loading, error };
}
Dicas práticas
- Sempre inclua as variáveis usadas dentro do efeito (
idno exemplo) na lista de dependências. O ESLint pluginreact-hooks/exhaustive-depsajuda a detectar ausências. - AbortController funciona em navegadores modernos e em Node (via
node-fetch). Se precisar de suporte a navegadores legados, use bibliotecas comoaxiosque já possuem cancelamento interno.
Exemplo 2 – setInterval com cleanup
Um contador que incrementa a cada segundo parece simples, mas se não limparmos o intervalo, o timer continuará rodando mesmo após o componente ser desmontado.
import { useEffect, useState } from 'react';
export function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const id = setInterval(() => setSeconds(s => s + 1), 1000);
// cleanup
return () => clearInterval(id);
}, []); // [] garante que o intervalo seja criado apenas uma vez
return <p>Tempo decorrido: {seconds}s</p>;
}
Ponto de atenção: se você colocar seconds na lista de dependências, o efeito será recriado a cada atualização, gerando múltiplos timers. Por isso, use a forma funcional do setState (s => s + 1) para acessar o valor mais recente sem precisar listar seconds como dependência.
Quando usar múltiplos useEffect
Não há problema algum em dividir a lógica em vários efeitos, cada um responsável por uma preocupação diferente. Isso aumenta a legibilidade e evita over‑engineering de dependências.
useEffect(() => {
// efeito A – escuta eventos de resize
const onResize = () => console.log(window.innerWidth);
window.addEventListener('resize', onResize);
return () => window.removeEventListener('resize', onResize);
}, []); // efeito A não depende de props/state
useEffect(() => {
// efeito B – busca dados quando `userId` mudar
fetchUser(userId);
}, [userId]); // efeito B depende apenas de userId
useRef para acessar DOM e valores persistentes
O que o useRef realmente armazena?
- Referência ao DOM – muito usado para focar inputs, medir elementos ou integrar bibliotecas de terceiros.
- Valor mutável que persiste entre renders – ao contrário de
useState, mudarref.currentnão dispara nova renderização.
Exemplo 1 – Focar um campo de texto ao montar
import { useEffect, useRef } from 'react';
export function SearchBox() {
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
inputRef.current?.focus(); // garante que o input receba foco
}, []); // roda só na montagem
return <input ref={inputRef} placeholder="Buscar..." />;
}
Exemplo 2 – Guardar o valor anterior de uma prop
Muitas vezes precisamos comparar o valor atual de uma prop com o anterior (por exemplo, para disparar animações). useRef permite armazenar o “valor anterior” sem causar re‑render.
import { useEffect, useRef } from 'react';
function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T>();
useEffect(() => {
ref.current = value;
}, [value]); // atualiza a cada mudança
return ref.current;
}
// Uso no componente
export function PriceTag({ price }: { price: number }) {
const prevPrice = usePrevious(price);
const changed = prevPrice !== undefined && price !== prevPrice;
return (
<div>
<span>Preço: R$ {price.toFixed(2)}</span>
{changed && <em> (alterado de R$ {prevPrice?.toFixed(2)})</em>}
</div>
);
}
Exemplo 3 – Contador de renderizações sem re‑render
import { useRef } from 'react';
export function RenderCounter() {
const renders = useRef(0);
renders.current += 1; // incrementa a cada render
return <p>Renderizações: {renders.current}</p>;
}
Curiosidade: Se você quiser que o valor seja exibido na UI, ainda precisará de
useState(ouuseReducer). OuseRefserve aqui apenas como instrumento de medição.
Dicas avançadas
- Evite usar
refcomo “state” – mudarref.currentnão atualiza a UI. Se a UI depende do valor, useuseState. - Referências a componentes de terceiros – ao integrar com bibliotecas como
Chart.jsouMapbox, crie o canvas ou container comrefe inicialize a biblioteca dentro de umuseEffectcom cleanup que destrua a instância. - Persistência entre rotas –
useRefpode guardar dados temporários que não precisam ser serializados no URL nem nolocalStorage. Ideal para armazenar “caches” de API que não justificam persistência completa.
Custom Hooks reais que você precisa conhecer
Criar custom hooks é a arte de extrair lógica reutilizável e isolar side‑effects. Quando bem projetados, eles tornam o código mais declarativo e testável.
1. useFetch – Busca genérica com estado de loading e erro
import { useEffect, useState } from 'react';
interface FetchState<T> {
data: T | null;
loading: boolean;
error: string | null;
}
/**
* Hook genérico para requisições GET.
* @param url URL da API
* @param options Opções de fetch (headers, etc.)
*/
export function useFetch<T>(url: string, options?: RequestInit) {
const [state, setState] = useState<FetchState<T>>({
data: null,
loading: true,
error: null,
});
useEffect(() => {
const controller = new AbortController();
async function load() {
setState(prev => ({ ...prev, loading: true }));
try {
const res = await fetch(url, { ...options, signal: controller.signal });
if (!res.ok) throw new Error(`Erro ${res.status}`);
const data = (await res.json()) as T;
setState({ data, loading: false, error: null });
} catch (e: any) {
if (e.name !== 'AbortError') {
setState({ data: null, loading: false, error: e.message });
}
}
}
load();
return () => controller.abort();
}, [url, JSON.stringify(options)]); // atenção ao serializar opções
return state;
}
Como usar
function UsersList() {
const { data: users, loading, error } = useFetch<User[]>('/api/users');
if (loading) return <p>Carregando...</p>;
if (error) return <p>Erro: {error}</p>;
return (
<ul>
{users?.map(u => (
<li key={u.id}>{u.name}</li>
))}
</ul>
);
}
Dica: Sempre inclua
JSON.stringify(options)nas dependências se quiser que mudanças em headers ou query params reinicializem a requisição. Caso as opções sejam estáticas, passe um objeto memoizado comuseMemo.
2. useLocalStorage – Sincroniza estado com localStorage
import { useState, useEffect } from 'react';
export function useLocalStorage<T>(key: string, initialValue: T) {
const [value, setValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? (JSON.parse(item) as T) : initialValue;
} catch {
return initialValue;
}
});
useEffect(() => {
try {
window.localStorage.setItem(key, JSON.stringify(value));
} catch {
// falha silenciosa – pode ser quota excedida
}
}, [key, value]);
return [value, setValue] as const;
}
Uso típico
function ThemeToggle() {
const [theme, setTheme] = useLocalStorage<'light' | 'dark'>('app-theme', 'light');
return (
<button onClick={() => setTheme(t => (t === 'light' ? 'dark' : 'light'))}>
Tema: {theme}
</button>
);
}
Boas práticas
- Persistência seletiva – nem todo estado merece ficar no
localStorage. Avalie se o dado será reutilizado entre sessões. - Versionamento – ao mudar a estrutura do objeto armazenado, considere limpar a chave ou migrar os dados para evitar
JSON.parsefalho.
3. useDebounce – Debounce de valores ou callbacks
Ideal para buscas “type‑ahead”, onde queremos esperar o usuário parar de digitar antes de disparar a requisição.
import { useEffect, useState } from 'react';
export function useDebounce<T>(value: T, delay = 300): T {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const handler = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(handler);
}, [value, delay]);
return debounced;
}
Exemplo de uso
function SearchInput() {
const [query, setQuery] = useState('');
const debouncedQuery = useDebounce(query, 500);
const { data: results, loading } = useFetch<Product[]>(
`/api/products?search=${encodeURIComponent(debouncedQuery)}`
);
return (
<>
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="Buscar produtos..."
/>
{loading && <p>Carregando...</p>}
<ul>
{results?.map(p => (
<li key={p.id}>{p.title}</li>
))}
</ul>
</>
);
}
Truque avançado: Se precisar de debounce de funções (não apenas valores), combine useCallback com useRef:
export function useDebouncedCallback<T extends unknown[]>(
callback: (...args: T) => void,
delay = 300
) {
const timeout = useRef<NodeJS.Timeout>();
return (...args: T) => {
if (timeout.current) clearTimeout(timeout.current);
timeout.current = setTimeout(() => callback(...args), delay);
};
}
Erros comuns e como evitá‑los
| Erro | Por que acontece | Como corrigir |
|---|---|---|
Dependências incompletas no useEffect | O ESLint pode ser desativado ou ignorado; desenvolvedores acham que “não muda, então não preciso”. | Deixe o plugin react-hooks/exhaustive-deps ativo. Quando a dependência é um objeto ou função, use useMemo ou useCallback para estabilizá‑la. |
| Loop infinito de renders | useEffect que altera estado que está na sua própria lista de dependências. | Separe a lógica: use um useRef para armazenar valores que não precisam disparar re‑render, ou mova a atualização de estado para outro efeito que não dependa do mesmo valor. |
| Stale closure (fechamento com valores antigos) | Funções criadas dentro do efeito capturam o estado no momento da criação e não são atualizadas. | Inclua as variáveis no array de dependências ou use a forma funcional de setState (setCount(c => c + 1)). |
Uso indevido de useRef como estado | Alterar ref.current não dispara render, levando a UI desatualizada. | Quando a UI depende do valor, use useState. Reserve useRef para “armazenamento mutável” ou acesso ao DOM. |
| Custom Hook que não limpa recursos | Bibliotecas externas (ex.: Chart.js, WebSocket) permanecem ativas após desmontar. | Sempre retorne uma função de limpeza no useEffect interno do hook. |
| Serialização de objetos complexos nas dependências | useEffect([obj]) sempre cria novo efeito, pois obj tem referência diferente a cada render. | Use useMemo(() => obj, [obj.prop1, obj.prop2]) ou transforme o objeto em string (JSON.stringify) se for pequeno e estável. |
| Excesso de abstração | Criar hooks para tudo pode gerar camadas desnecessárias |
Glossário do post
- react
- Biblioteca JavaScript para construir interfaces de usuário a partir de componentes reutilizáveis, mantida pela Meta.
- dom
- Document Object Model: representação em árvore da página que o navegador expõe, permitindo que JavaScript leia e modifique o conteúdo.
- javascript
- Linguagem de programação que roda no navegador (e em servidores via Node.js), responsável pela interatividade das páginas web.
- eslint
- Ferramenta de linting que analisa código JavaScript/TypeScript procurando erros e padrões problemáticos.
- node
- Node.js: runtime JavaScript fora do navegador, baseado no motor V8, usado principalmente para construir servidores e ferramentas de linha de comando.
- ui
- User Interface: a camada visual com a qual o usuário interage diretamente em um aplicativo ou site.
- url
- Uniform Resource Locator: o endereço único que localiza um recurso na web.
- api
- Application Programming Interface: conjunto de regras que permite a um programa conversar com outro, expondo dados e funcionalidades de forma padronizada.
- json
- JavaScript Object Notation: formato leve de troca de dados baseado em pares chave/valor, legível para humanos e máquinas.
- closure
- Função que captura e mantém acesso às variáveis do escopo onde foi criada, mesmo após esse escopo terminar.
- websocket
- Protocolo que mantém uma conexão bidirecional contínua entre cliente e servidor, permitindo comunicação em tempo real.
