Erros com React Hooks que derrubam código em produção (e como corrigir)
← Voltar para Codeshort

Erros com React Hooks que derrubam código em produção (e como corrigir)

useEffect com closure stale, useState desnecessário, useMemo sem sentido — veja os erros reais que qualquer dev comete e como identificar antes do PR.

DC
Dev Code Software
11 de maio de 2026·9 min de leitura

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-hooks no projeto. A regra exhaustive-deps pega 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:

  1. Estados derivados — qualquer useState que é sincronizado por um useEffect com outra variável é candidato a virar uma constante calculada na renderização
  2. Arrays de dependência — revise cada useEffect: o que ele usa dentro precisa estar nas dependências. Configure eslint-plugin-react-hooks se ainda não tiver
  3. Objetos e funções inline como dependências — extraia valores primitivos ou envolva com useMemo/useCallback quando precisar de referência estável
  4. Forma funcional do setState — dentro de callbacks assíncronos, timeouts e intervals, prefira setState(prev => ...) em vez de setState(value + 1)
  5. useCallback e useMemo com propósito — confirme que existe um React.memo do outro lado ou uma dependência real que justifique a memoização
  6. useRef para valores internos — se um valor não precisa disparar re-render, useRef é mais correto que useState

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.