- O modelo mental que ninguém explica direito
- useState para valor que não é estado
- useEffect: dependências erradas e loops silenciosos
- Closure stale: o bug que o revisor encontra, não você
- useCallback e useMemo: quando ajudam e quando atrapalham
- useRef como container de valor mutável
- Hooks customizados: a regra dos três usos
- FAQ
- Checklist de revisão
O modelo mental que ninguém explica direito
Hooks existem desde o React 16.8. A documentação é boa. Mesmo assim, toda semana aparece um bug de closure stale em produção, um loop infinito causado por objeto no array de dependências, ou um useCallback que não evita nenhum re-render.
Não é falta de leitura. O problema é que hooks exigem um modelo mental diferente do que a maioria aprendeu com classes — e esse modelo raramente é ensinado de forma explícita.
A ideia central: cada renderização é um snapshot isolado. Props, estado, funções definidas no componente — tudo pertence àquela renderização. O useEffect que você registra numa renderização vai enxergar os valores daquela renderização, não dos renders seguintes.
Quando você entende isso, os erros mais comuns deixam de ser misteriosos.
useState para valor que não é estado
O erro não é usar useState incorretamente. É usar quando o valor nem deveria ser estado.
Sintoma clássico: um useState que só existe para ser sincronizado por um useEffect.
function UserCard({ firstName, lastName }) {
const [fullName, setFullName] = useState(`${firstName} ${lastName}`);
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
return <p>{fullName}</p>;
}
Esse código renderiza três vezes para mostrar um nome completo. Na primeira renderização, fullName tem o valor inicial. Depois o effect roda, chama setFullName, provoca outro render. Se as props mudarem, repete o ciclo.
A correção é simplesmente não criar o estado:
function UserCard({ firstName, lastName }) {
const fullName = `${firstName} ${lastName}`;
return <p>{fullName}</p>;
}
Regra prática: se um valor pode ser calculado a partir de props ou de outro estado em tempo de renderização, ele não é estado — é uma variável derivada. Coloque direto no corpo da função.
💡 Dica: Antes de escrever
useState, responda: "esse valor existe de forma independente, ou é sempre uma consequência de outro dado?" Se for consequência, calcule na renderização. Sem estado, sem effect, sem bug.
O mesmo raciocínio vale para useMemo. Um cálculo simples — pegar duas letras de uma string, somar dois números — não precisa ser memoizado. O overhead do useMemo vai ser maior que o cálculo em si.
useEffect: dependências erradas e loops silenciosos
Esse é o campeão de aparições em code review.
Dependência faltando
function SearchResults({ query }) {
const [results, setResults] = useState([]);
useEffect(() => {
fetchResults(query).then(setResults);
}, []);
return <ResultList items={results} />;
}
O array vazio diz ao React: "rode isso uma vez, na montagem". O problema é que query muda e o effect não sabe. O componente vai exibir para sempre os resultados da primeira busca.
useEffect(() => {
fetchResults(query).then(setResults);
}, [query]);
Adicionar query como dependência parece óbvio, mas em componentes maiores — com vários estados, props e callbacks — é fácil esquecer.
Objeto ou função inline como dependência
useEffect(() => {
doSomething(options);
}, [{ page: 1, limit: 10 }]);
JavaScript compara objetos por referência, não por valor. { page: 1 } na renderização anterior e { page: 1 } na atual são dois objetos diferentes na memória. O React vai achar que a dependência mudou em todo render — e rodar o effect infinitamente.
const page = 1;
const limit = 10;
useEffect(() => {
doSomething({ page, limit });
}, [page, limit]);
Extraia os valores primitivos. Se o objeto vem de fora do componente ou de um useMemo com referência estável, aí pode usar direto.
⚠️ Atenção: Configure o
eslint-plugin-react-hooksno projeto. A regraexhaustive-depspega boa parte dos casos de dependência faltando. Não resolve tudo, mas elimina os erros mais óbvios antes do commit.
Closure stale: o bug que o revisor encontra, não você
Esse é o mais traiçoeiro. O componente parece funcionar em testes rápidos. Só mostra o problema em condições específicas de tempo ou interação.
function Timer() {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
console.log(count);
setCount(count + 1);
}, 1000);
return () => clearInterval(interval);
}, []);
return <p>{count}</p>;
}
O array vazio faz o effect rodar uma vez. O callback do setInterval é criado nessa renderização e "fecha" sobre o valor de count naquele momento — que é 0. Nos ticks seguintes, o interval continua somando 0 + 1 = 1, eternamente. O console.log vai mostrar 0 para sempre, independente do que aparece na tela.
A solução está na forma funcional do setState:
useEffect(() => {
const interval = setInterval(() => {
setCount(prev => prev + 1);
}, 1000);
return () => clearInterval(interval);
}, []);
Quando você passa uma função para setState, o React chama ela com o valor atual do estado, não o capturado no closure. Não importa quando o callback foi criado — ele sempre recebe o valor correto.
Use a forma funcional sempre que o próximo estado depender do anterior: dentro de setInterval, setTimeout, event listeners adicionados manualmente e callbacks de promessas de longa duração.
useCallback e useMemo: quando ajudam e quando atrapalham
Depois de descobrir useCallback e useMemo, a tentação é usá-los em tudo. Resultado: código mais complexo, mesma performance — às vezes pior.
Quando useMemo faz sentido
const filteredList = useMemo(() => {
return items.filter(item => item.category === activeCategory);
}, [items, activeCategory]);
Filtrar um array grande a cada render pode ser caro. Se items tem milhares de entradas e o componente re-renderiza com frequência por outros motivos, useMemo evita o trabalho desnecessário.
Quando não faz sentido:
const initials = useMemo(() => name.slice(0, 2).toUpperCase(), [name]);
O useMemo tem overhead de criação, comparação de dependências e armazenamento. Pegar duas letras de uma string custa menos que tudo isso somado.
Quando useCallback faz sentido
useCallback só evita re-renders quando combinado com React.memo no componente filho:
const handleSubmit = useCallback((data) => {
submitForm(data);
}, [submitForm]);
const MemoizedForm = React.memo(({ onSubmit }) => {
return <form onSubmit={onSubmit}>...</form>;
});
Sem o React.memo, o filho re-renderiza de qualquer forma — o useCallback não muda nada.
O outro caso válido: quando a função entra como dependência de um useEffect. Sem useCallback, a função recria a cada render e o effect roda toda vez.
const fetchData = useCallback(() => {
return api.get(`/users/${userId}`);
}, [userId]);
useEffect(() => {
fetchData().then(setUser);
}, [fetchData]);
useRef como container de valor mutável
O uso mais conhecido do useRef é acessar elementos DOM. Mas tem um segundo papel que resolve um problema específico: guardar um valor que precisa persistir entre renders sem disparar re-render.
useState e useRef guardam valores entre renders. A diferença é que mutar ref.current não agenda um novo render.
Caso prático — autosave com debounce:
function AutoSave({ content }) {
const timerRef = useRef(null);
useEffect(() => {
if (timerRef.current) clearTimeout(timerRef.current);
timerRef.current = setTimeout(() => {
saveContent(content);
}, 1000);
return () => clearTimeout(timerRef.current);
}, [content]);
}
Se você usasse useState para guardar o ID do timeout, cada digitação dispararia um re-render só para atualizar um valor interno que o usuário nunca vê. Com useRef, você lê e escreve sem tocar no ciclo de renderização.
Outro padrão útil: acessar o valor anterior de uma prop ou estado.
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
O effect sem dependências roda após cada render e atualiza ref.current. Na próxima renderização, ref.current ainda tem o valor anterior — o useEffect ainda não rodou. Funciona por causa da ordem: render → return → effect.
Hooks customizados: a regra dos três usos
Extrair um hook customizado é criar uma abstração. Abstrações têm custo: mais um arquivo, mais um nível de indireção, mais um lugar para procurar quando algo quebra.
Vale a pena quando a lógica aparece em pelo menos dois ou três lugares diferentes — ou quando o ciclo de vida do efeito faz sentido como unidade isolada.
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
useDebounce vai aparecer em campos de busca, filtros, validações em tempo real — faz todo sentido existir como abstração reutilizável.
Agora um useFetchUserData que encapsula três linhas de useEffect usadas em um único componente? É abstração por aparência, não por necessidade. Deixa o useEffect no componente, onde o contexto está claro.
Critérios para extrair:
- A lógica aparece em dois ou mais componentes não relacionados
- O conjunto de hooks tem um ciclo de vida coeso que faz sentido nomeado
- O componente está crescendo por causa de lógica, não de JSX
Critérios para não extrair:
- É usado em um único lugar
- O nome do hook seria mais longo que o código que ele esconde
- A abstração obriga quem lê a pular de arquivo para entender o fluxo
FAQ
Posso usar hooks dentro de condicionais ou loops?
Não. O React rastreia hooks pela ordem em que são chamados. Se um render pula um hook ou chama um diferente, a ordem muda e o estado vai para o hook errado — o erro costuma ser silencioso e difícil de rastrear. A lógica condicional fica dentro do hook, não ao redor dele.
Por que useEffect roda duas vezes no desenvolvimento com React 18?
O Strict Mode do React 18 monta, desmonta e remonta intencionalmente cada componente para revelar efeitos colaterais que não fazem cleanup correto. Em produção, roda uma vez. Se seu código quebra com dois mounts, a função de cleanup está faltando ou incompleta — corrija isso antes de desativar o Strict Mode.
Qual a diferença prática entre useEffect e useLayoutEffect?
useEffect roda de forma assíncrona, depois que o browser pintou a tela — ideal para a maioria dos casos. useLayoutEffect roda de forma síncrona, bloqueando a pintura, antes de o usuário ver qualquer coisa. Use useLayoutEffect só quando precisar medir ou ajustar o DOM antes de exibir — evitar flash de posição, por exemplo. O resto usa useEffect.
Objeto único no useState ou vários useState separados?
Quando os valores mudam juntos e representam um estado coeso — campos de um formulário, parâmetros de filtro relacionados — um objeto faz sentido. Quando são independentes, useState separado é mais claro e evita que uma mudança acione quem não precisa ser atualizado. Não existe regra absoluta: use o que comunicar melhor a intenção do código.
useReducer substitui useState quando o estado fica complexo?
Para transições com múltiplos casos, onde o próximo estado depende do anterior de formas diferentes, useReducer deixa o fluxo explícito e testável. Para estado simples e independente, useState é mais legível e direto. O sinal para migrar costuma ser: quando você tem vários setState juntos que precisam ser atômicos, ou quando a lógica de transição começa a crescer no corpo do componente.
Checklist de revisão
Antes do próximo PR, passe por esses pontos no código que você mantém:
- Estados derivados — qualquer
useStateque é sincronizado por umuseEffectcom outra variável é candidato a virar uma constante calculada na renderização - Arrays de dependência — revise cada
useEffect: o que ele usa dentro precisa estar nas dependências. Configureeslint-plugin-react-hooksse ainda não tiver - Objetos e funções inline como dependências — extraia valores primitivos ou envolva com
useMemo/useCallbackquando precisar de referência estável - Forma funcional do setState — dentro de callbacks assíncronos, timeouts e intervals, prefira
setState(prev => ...)em vez desetState(value + 1) - useCallback e useMemo com propósito — confirme que existe um
React.memodo outro lado ou uma dependência real que justifique a memoização - useRef para valores internos — se um valor não precisa disparar re-render,
useRefé mais correto queuseState
Hooks são uma API pequena. O modelo mental por trás deles não é. Quando os dois se alinham, o código fica previsível — e bugs de timing viram raridade.