No artigo anterior você aprendeu useState e useEffect — os dois hooks mais usados. Mas o React tem um ecossistema rico de hooks que resolvem problemas específicos de performance, compartilhamento de estado e lógica reutilizável.
Neste artigo vamos explorar useContext, useReducer, useMemo, useCallback, useRef, useId e — o mais poderoso de todos — como criar seus próprios hooks customizados.
Regras dos Hooks
Antes de começar, as regras que nunca podem ser quebradas:
// ✅ Correto — hooks no nível do componente
function MeuComponente() {
const [estado, setEstado] = useState(0);
useEffect(() => {}, []);
return <div>{estado}</div>;
}
// ❌ Nunca dentro de condicionais
function Errado() {
if (condicao) {
const [estado, setEstado] = useState(0); // ❌
}
}
// ❌ Nunca dentro de loops
function Errado() {
for (let i = 0; i < 3; i++) {
const [estado, setEstado] = useState(0); // ❌
}
}
// ❌ Nunca em funções comuns — apenas em componentes e hooks customizados
function minhaFuncao() {
const [estado, setEstado] = useState(0); // ❌
}
// A razão: React depende da ordem de chamada dos hooks
// para associar estado ao componente correto
useRef — referências sem re-renderização
useRef cria um objeto mutável que persiste entre renderizações sem causar re-renderização:
import { useRef, useState, useEffect } from 'react';
// ── Caso 1: Referência ao elemento DOM ─────────────
function CampoAutoFoco() {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current?.focus(); // acessa o elemento DOM diretamente
}, []);
return <input ref={inputRef} placeholder="Foca automaticamente" />;
}
// ── Caso 2: Guardar valor sem re-renderizar ─────────
function Cronometro() {
const [tempo, setTempo] = useState(0);
const [rodando, setRodando] = useState(false);
const intervalRef = useRef(null);
function iniciar() {
if (rodando) return;
setRodando(true);
intervalRef.current = setInterval(() => {
setTempo(t => t + 1);
}, 1000);
}
function pausar() {
clearInterval(intervalRef.current);
setRodando(false);
}
function resetar() {
clearInterval(intervalRef.current);
setRodando(false);
setTempo(0);
}
// Limpa o intervalo quando o componente desmonta
useEffect(() => () => clearInterval(intervalRef.current), []);
return (
<div>
<p>{String(Math.floor(tempo / 60)).padStart(2, '0')}:{String(tempo % 60).padStart(2, '0')}</p>
<button onClick={iniciar} disabled={rodando}>▶ Iniciar</button>
<button onClick={pausar} disabled={!rodando}>⏸ Pausar</button>
<button onClick={resetar}>↺ Resetar</button>
</div>
);
}
// ── Caso 3: Valor anterior ─────────────────────────
function usePrevious(valor) {
const ref = useRef(undefined);
useEffect(() => { ref.current = valor; });
return ref.current; // retorna o valor da renderização anterior
}
function Contador() {
const [count, setCount] = useState(0);
const anterior = usePrevious(count);
return (
<div>
<p>Atual: {count} | Anterior: {anterior ?? 'nenhum'}</p>
<button onClick={() => setCount(c => c + 1)}>+</button>
</div>
);
}
useContext — estado global sem prop drilling
O problema que o Context resolve:
// ── Prop drilling — o problema ─────────────────────
// App passa tema para Layout que passa para Sidebar
// que passa para Menu que passa para MenuItem
// só para que MenuItem use o tema. Insuportável.
function App() {
const [tema, setTema] = useState('claro');
return <Layout tema={tema} setTema={setTema} />;
}
function Layout({ tema, setTema }) {
return <Sidebar tema={tema} setTema={setTema} />;
}
function Sidebar({ tema, setTema }) {
return <Menu tema={tema} setTema={setTema} />;
}
// ... cinco níveis depois ...
function MenuItem({ tema }) {
return <div className={`item tema-${tema}`}>...</div>;
}
// ── Context — a solução ────────────────────────────
import { createContext, useContext, useState } from 'react';
// 1. Criar o contexto com valor padrão
const TemaContext = createContext({
tema: 'claro',
alternarTema: () => {},
});
// 2. Criar o Provider — envolve os componentes que precisam do valor
function TemaProvider({ children }) {
const [tema, setTema] = useState('claro');
function alternarTema() {
setTema(t => t === 'claro' ? 'escuro' : 'claro');
}
return (
<TemaContext.Provider value={{ tema, alternarTema }}>
{children}
</TemaContext.Provider>
);
}
// Hook customizado para facilitar o uso
function useTema() {
const contexto = useContext(TemaContext);
if (!contexto) throw new Error('useTema deve ser usado dentro de TemaProvider');
return contexto;
}
// 3. Usar em qualquer componente dentro do Provider — sem props
function BotaoTema() {
const { tema, alternarTema } = useTema();
return (
<button onClick={alternarTema}>
{tema === 'claro' ? '🌙 Modo escuro' : '☀️ Modo claro'}
</button>
);
}
function MenuItem() {
const { tema } = useTema();
return <div className={`item tema-${tema}`}>...</div>;
}
// 4. Aplicar o Provider no topo
function App() {
return (
<TemaProvider>
<Layout /> {/* BotaoTema e MenuItem acessam o contexto diretamente */}
</TemaProvider>
);
}
useReducer — estado complexo com lógica centralizada
Quando o estado tem muitas transições inter-relacionadas, useReducer é mais adequado que múltiplos useState:
import { useReducer } from 'react';
// Estado inicial
const estadoInicial = {
tarefas: [],
filtro: 'todas',
carregando: false,
erro: null,
};
// Reducer — função pura: (estado, ação) → novo estado
function tarefasReducer(estado, acao) {
switch (acao.type) {
case 'ADICIONAR':
return {
...estado,
tarefas: [
{ id: crypto.randomUUID(), texto: acao.payload, feita: false },
...estado.tarefas,
],
};
case 'ALTERNAR':
return {
...estado,
tarefas: estado.tarefas.map(t =>
t.id === acao.payload ? { ...t, feita: !t.feita } : t
),
};
case 'REMOVER':
return {
...estado,
tarefas: estado.tarefas.filter(t => t.id !== acao.payload),
};
case 'DEFINIR_FILTRO':
return { ...estado, filtro: acao.payload };
case 'LIMPAR_CONCLUIDAS':
return { ...estado, tarefas: estado.tarefas.filter(t => !t.feita) };
case 'CARREGAR_INICIO':
return { ...estado, carregando: true, erro: null };
case 'CARREGAR_SUCESSO':
return { ...estado, carregando: false, tarefas: acao.payload };
case 'CARREGAR_ERRO':
return { ...estado, carregando: false, erro: acao.payload };
default:
return estado;
}
}
// Componente usando useReducer
function AppTarefas() {
const [estado, dispatch] = useReducer(tarefasReducer, estadoInicial);
// Ações tipadas — evita erros de digitação
const acoes = {
adicionar: (texto) => dispatch({ type: 'ADICIONAR', payload: texto }),
alternar: (id) => dispatch({ type: 'ALTERNAR', payload: id }),
remover: (id) => dispatch({ type: 'REMOVER', payload: id }),
filtrar: (filtro) => dispatch({ type: 'DEFINIR_FILTRO', payload: filtro }),
limparConcluidas: () => dispatch({ type: 'LIMPAR_CONCLUIDAS' }),
};
const tarefasFiltradas = estado.tarefas.filter(t => {
if (estado.filtro === 'pendentes') return !t.feita;
if (estado.filtro === 'concluidas') return t.feita;
return true;
});
return (
<div>
{estado.erro && <p className="erro">{estado.erro}</p>}
{estado.carregando && <p>Carregando...</p>}
<ul>
{tarefasFiltradas.map(t => (
<li key={t.id}>
<input
type="checkbox"
checked={t.feita}
onChange={() => acoes.alternar(t.id)}
/>
{t.texto}
<button onClick={() => acoes.remover(t.id)}>🗑</button>
</li>
))}
</ul>
</div>
);
}
useMemo — memorizando valores calculados
import { useState, useMemo } from 'react';
function ListaFiltrada({ produtos }) {
const [busca, setBusca] = useState('');
const [ordenar, setOrdenar] = useState('nome');
// SEM useMemo — filtra e ordena em TODA renderização
// (mesmo quando só a UI mudou, não os produtos ou busca)
const produtosProcessados = produtos
.filter(p => p.nome.toLowerCase().includes(busca.toLowerCase()))
.sort((a, b) => a[ordenar] > b[ordenar] ? 1 : -1);
// COM useMemo — só recalcula quando produtos, busca ou ordenar mudam
const produtosMemo = useMemo(() => {
console.log('Recalculando lista...');
return produtos
.filter(p => p.nome.toLowerCase().includes(busca.toLowerCase()))
.sort((a, b) => a[ordenar] > b[ordenar] ? 1 : -1);
}, [produtos, busca, ordenar]); // dependências
return (
<div>
<input value={busca} onChange={e => setBusca(e.target.value)} />
<select value={ordenar} onChange={e => setOrdenar(e.target.value)}>
<option value="nome">Nome</option>
<option value="preco">Preço</option>
</select>
<ul>
{produtosMemo.map(p => <li key={p.id}>{p.nome} — R$ {p.preco}</li>)}
</ul>
</div>
);
}
// useMemo para objetos passados como props (evita re-renders desnecessários)
function Configuracao({ usuario }) {
const config = useMemo(() => ({
tema: usuario.preferencias.tema,
idioma: usuario.preferencias.idioma,
formato: usuario.preferencias.formatoData,
}), [usuario.preferencias]);
return <Painel config={config} />;
}
// ⚠️ Não use useMemo para tudo — tem custo de memória
// Use apenas quando:
// 1. O cálculo é realmente custoso (lista grande, agregação complexa)
// 2. O valor é passado para componentes filhos memorizados
useCallback — memorizando funções
import { useState, useCallback, memo } from 'react';
// React.memo — componente que só re-renderiza se as props mudarem
const BotaoItem = memo(function BotaoItem({ id, onRemover }) {
console.log(`BotaoItem ${id} renderizou`);
return <button onClick={() => onRemover(id)}>Remover {id}</button>;
});
function ListaItens() {
const [itens, setItens] = useState([1, 2, 3, 4, 5]);
const [contador, setContador] = useState(0);
// SEM useCallback — nova função a cada renderização
// → BotaoItem re-renderiza mesmo sem mudança nos itens!
const removerSemMemo = (id) => setItens(prev => prev.filter(i => i !== id));
// COM useCallback — mesma referência de função entre renderizações
// → BotaoItem só re-renderiza quando itens muda
const removerComMemo = useCallback((id) => {
setItens(prev => prev.filter(i => i !== id));
}, []); // [] porque usa o padrão funcional do setState
return (
<div>
<button onClick={() => setContador(c => c + 1)}>
Contador: {contador} {/* clique aqui não deve re-renderizar BotaoItem */}
</button>
{itens.map(id => (
<BotaoItem key={id} id={id} onRemover={removerComMemo} />
))}
</div>
);
}
// useCallback para funções passadas como dependência de useEffect
function BuscaAoVivo({ termo }) {
const [resultados, setResultados] = useState([]);
const buscar = useCallback(async () => {
if (!termo) return;
const res = await fetch(`/api/busca?q=${termo}`);
setResultados(await res.json());
}, [termo]); // nova função apenas quando termo muda
useEffect(() => {
buscar();
}, [buscar]); // ESLint fica feliz — dependência explícita
return <ul>{resultados.map(r => <li key={r.id}>{r.nome}</li>)}</ul>;
}
useId — IDs únicos acessíveis
import { useId } from 'react';
// Gera IDs únicos consistentes entre servidor e cliente (SSR-safe)
function CampoFormulario({ label, tipo = 'text', ...props }) {
const id = useId(); // ex: ":r0:", ":r1:", ":r2:"...
return (
<div className="campo">
<label htmlFor={id}>{label}</label>
<input id={id} type={tipo} {...props} />
</div>
);
}
// Múltiplos IDs em um componente
function RangeComLabel({ label, min, max, value, onChange }) {
const baseId = useId();
const inputId = `${baseId}-input`;
const listaId = `${baseId}-lista`;
return (
<div>
<label htmlFor={inputId}>{label}: {value}</label>
<input
id={inputId}
type="range"
min={min}
max={max}
value={value}
onChange={onChange}
aria-describedby={listaId}
/>
</div>
);
}
Hooks customizados — reutilizando lógica
Hooks customizados são funções que começam com use e podem chamar outros hooks. São a forma mais poderosa de reutilização de lógica no React:
// ── useFetch — busca de dados reutilizável ─────────
function useFetch(url) {
const [dados, setDados] = useState(null);
const [carregando, setCarregando] = useState(true);
const [erro, setErro] = useState(null);
useEffect(() => {
let cancelado = false;
const controller = new AbortController();
async function buscar() {
setCarregando(true);
setErro(null);
try {
const res = await fetch(url, { signal: controller.signal });
if (!res.ok) throw new Error(`Erro ${res.status}`);
const json = await res.json();
if (!cancelado) setDados(json);
} catch (e) {
if (!cancelado && e.name !== 'AbortError') {
setErro(e.message);
}
} finally {
if (!cancelado) setCarregando(false);
}
}
buscar();
return () => {
cancelado = true;
controller.abort();
};
}, [url]);
return { dados, carregando, erro };
}
// Uso
function PerfilUsuario({ id }) {
const { dados, carregando, erro } = useFetch(
`https://jsonplaceholder.typicode.com/users/${id}`
);
if (carregando) return <p>Carregando...</p>;
if (erro) return <p>Erro: {erro}</p>;
return <div><h2>{dados?.name}</h2><p>{dados?.email}</p></div>;
}
// ── useLocalStorage — persistência automática ──────
function useLocalStorage(chave, valorInicial) {
const [valor, setValor] = useState(() => {
try {
const item = localStorage.getItem(chave);
return item !== null ? JSON.parse(item) : valorInicial;
} catch {
return valorInicial;
}
});
const definir = useCallback((novoValor) => {
try {
// Permite passar função (igual ao setState)
const valorParaSalvar =
typeof novoValor === 'function' ? novoValor(valor) : novoValor;
setValor(valorParaSalvar);
localStorage.setItem(chave, JSON.stringify(valorParaSalvar));
} catch (e) {
console.warn(`useLocalStorage: erro ao salvar "${chave}"`, e);
}
}, [chave, valor]);
return [valor, definir];
}
// Uso — idêntico ao useState, mas persiste!
function Preferencias() {
const [tema, setTema] = useLocalStorage('tema', 'claro');
const [idioma, setIdioma] = useLocalStorage('idioma', 'pt-BR');
return (
<div>
<select value={tema} onChange={e => setTema(e.target.value)}>
<option value="claro">☀️ Claro</option>
<option value="escuro">🌙 Escuro</option>
</select>
</div>
);
}
// ── useDebounce — atraso de valor ──────────────────
function useDebounce(valor, delay = 500) {
const [valorAtrasado, setValorAtrasado] = useState(valor);
useEffect(() => {
const timer = setTimeout(() => setValorAtrasado(valor), delay);
return () => clearTimeout(timer);
}, [valor, delay]);
return valorAtrasado;
}
// ── useWindowSize — dimensões da janela ────────────
function useWindowSize() {
const [tamanho, setTamanho] = useState({
largura: window.innerWidth,
altura: window.innerHeight,
});
useEffect(() => {
function handleResize() {
setTamanho({ largura: window.innerWidth, altura: window.innerHeight });
}
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return tamanho;
}
// ── useOnClickOutside — fechar ao clicar fora ──────
function useOnClickOutside(ref, handler) {
useEffect(() => {
function listener(evento) {
if (!ref.current || ref.current.contains(evento.target)) return;
handler(evento);
}
document.addEventListener('mousedown', listener);
document.addEventListener('touchstart', listener);
return () => {
document.removeEventListener('mousedown', listener);
document.removeEventListener('touchstart', listener);
};
}, [ref, handler]);
}
// ── useMediaQuery — responsive no código ───────────
function useMediaQuery(query) {
const [corresponde, setCorresponde] = useState(
() => window.matchMedia(query).matches
);
useEffect(() => {
const media = window.matchMedia(query);
function handleChange(e) { setCorresponde(e.matches); }
media.addEventListener('change', handleChange);
return () => media.removeEventListener('change', handleChange);
}, [query]);
return corresponde;
}
// Uso
function Layout() {
const eMobile = useMediaQuery('(max-width: 768px)');
const prefereEscuro = useMediaQuery('(prefers-color-scheme: dark)');
return (
<div>
{eMobile ? <MenuMobile /> : <MenuDesktop />}
{prefereEscuro && <p>Você prefere o modo escuro!</p>}
</div>
);
}
// ── useForm — gerenciamento completo de formulário ──
function useForm(camposIniciais, validar) {
const [valores, setValores] = useState(camposIniciais);
const [erros, setErros] = useState({});
const [tocados, setTocados] = useState({});
const [enviando, setEnviando] = useState(false);
function handleChange(e) {
const { name, value, type, checked } = e.target;
const novoValor = type === 'checkbox' ? checked : value;
setValores(prev => ({ ...prev, [name]: novoValor }));
// Valida campo em tempo real se já foi tocado
if (tocados[name] && validar) {
const novosErros = validar({ ...valores, [name]: novoValor });
setErros(prev => ({ ...prev, [name]: novosErros[name] }));
}
}
function handleBlur(e) {
const { name } = e.target;
setTocados(prev => ({ ...prev, [name]: true }));
if (validar) {
const novosErros = validar(valores);
setErros(prev => ({ ...prev, [name]: novosErros[name] }));
}
}
async function handleSubmit(onSubmit) {
return async (e) => {
e.preventDefault();
// Marca todos como tocados e valida
const todosTocados = Object.keys(camposIniciais).reduce(
(acc, k) => ({ ...acc, [k]: true }), {}
);
setTocados(todosTocados);
if (validar) {
const novosErros = validar(valores);
setErros(novosErros);
if (Object.values(novosErros).some(Boolean)) return;
}
setEnviando(true);
try {
await onSubmit(valores);
} finally {
setEnviando(false);
}
};
}
function resetar() {
setValores(camposIniciais);
setErros({});
setTocados({});
}
return {
valores,
erros,
tocados,
enviando,
handleChange,
handleBlur,
handleSubmit,
resetar,
};
}
// Uso
function FormularioCadastro() {
const { valores, erros, tocados, enviando, handleChange, handleBlur, handleSubmit } =
useForm(
{ nome: '', email: '', senha: '' },
(v) => ({
nome: !v.nome ? 'Nome é obrigatório.' : '',
email: !v.email.includes('@') ? 'Email inválido.' : '',
senha: v.senha.length < 6 ? 'Mínimo 6 caracteres.' : '',
})
);
const aoEnviar = handleSubmit(async (dados) => {
await fetch('/api/usuarios', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(dados),
});
});
return (
<form onSubmit={aoEnviar}>
<input
name="nome"
value={valores.nome}
onChange={handleChange}
onBlur={handleBlur}
/>
{tocados.nome && erros.nome && <span className="erro">{erros.nome}</span>}
<input
name="email"
type="email"
value={valores.email}
onChange={handleChange}
onBlur={handleBlur}
/>
{tocados.email && erros.email && <span className="erro">{erros.email}</span>}
<input
name="senha"
type="password"
value={valores.senha}
onChange={handleChange}
onBlur={handleBlur}
/>
{tocados.senha && erros.senha && <span className="erro">{erros.senha}</span>}
<button type="submit" disabled={enviando}>
{enviando ? 'Enviando...' : 'Cadastrar'}
</button>
</form>
);
}
Quando usar cada hook
┌──────────────────────────────────────────────────────────┐
│ GUIA DE DECISÃO │
│ │
│ useState → estado simples, independente │
│ useReducer → estado complexo com múltiplas ações │
│ useContext → estado global / compartilhado │
│ useRef → DOM direto / valor sem re-render │
│ useEffect → efeitos colaterais / fetch / timers │
│ useMemo → cálculo custoso memorizado │
│ useCallback → função memorizada (para memo/Effect) │
│ useId → IDs únicos para acessibilidade │
│ Hook custom → lógica reutilizável entre componentes │
│ │
│ ⚠️ Não otimize prematuramente: │
│ useMemo e useCallback têm custo. │
│ Só use quando há problema de performance real. │
└──────────────────────────────────────────────────────────┘
Tarefa para você
Construa uma biblioteca de hooks customizados para o projeto:
// 1. useContador(inicial, { min, max, passo })
// - increment, decrement, resetar
// - respeita min e max
// - passo configurável
// 2. useToggle(valorInicial)
// - toggle, ativar, desativar
// - retorna [valor, { toggle, ativar, desativar }]
// 3. useAsync(funcaoAssincrona, dependencias)
// - executa a função automaticamente
// - retorna { dados, carregando, erro, executar }
// - executar() permite refazer manualmente
// 4. useCopiarTexto()
// - retorna [copiado, copiar]
// - copiar(texto) copia para clipboard
// - copiado fica true por 2 segundos depois volta false
// 5. useHistorico(valorInicial)
// - rastreia histórico de mudanças
// - retorna [atual, { definir, desfazer, refazer, limpar }]
// - desfazer e refazer navegam pelo histórico
// Para cada hook: escreva testes com Jest
// (simule o hook com @testing-library/react-hooks
// ou renderize um componente de teste)
Conclusão
Neste artigo você aprendeu:
- As regras imutáveis dos hooks e por que existem
useRef— referências ao DOM e valores persistentes sem re-renderizaçãouseContext— eliminando prop drilling com estado globaluseReducer— gerenciamento de estado complexo com reducer purouseMemo— memorizando valores calculados para performanceuseCallback— memorizando funções para evitar re-rendersuseId— gerando IDs únicos acessíveis e SSR-safe- Como criar hooks customizados — a forma mais poderosa de reutilização
- Hooks prontos para uso:
useFetch,useLocalStorage,useDebounce,useWindowSize,useOnClickOutside,useMediaQuery,useForm - O guia de decisão — quando usar cada hook
No próximo artigo vamos aprender gerenciamento de estado com Zustand e como conectar nossa API do back-end ao front-end React com React Query.
📌 Próximo artigo: Aula 40 — Estado global com Zustand e React Query
📚 Fontes e Referências
- React — Hooks Reference: https://react.dev/reference/react
- React — useContext: https://react.dev/reference/react/useContext
- React — useReducer: https://react.dev/reference/react/useReducer
- React — useMemo: https://react.dev/reference/react/useMemo
- React — useCallback: https://react.dev/reference/react/useCallback
- React — Custom Hooks: https://react.dev/learn/reusing-logic-with-custom-hooks
- usehooks.com — Coleção de hooks customizados: https://usehooks.com
- React Hooks in Action — John Larsen (Manning)
- Learning React — Alex Banks e Eve Porcello (O'Reilly)
- roadmap.sh — React: https://roadmap.sh/react