Rendered more hooks than during the previous render
O erro que aparece quando um hook fica dentro de if ou depois de um return. Como o React conta hooks por ordem e como reorganizar o componente.
Esse erro quer dizer uma coisa só: nesta renderização o componente chamou mais hooks do que na renderização anterior. O React não guarda hook por nome — ele guarda por posição na ordem de chamada. Se a quantidade muda no meio do caminho, ele interrompe o render e joga o erro.
Os exemplos deste artigo são todos do painel da Clínica Patas, uma clínica veterinária: a agenda do dia, a ficha do paciente, o botão de confirmar consulta. Rodei tudo no Node 24.16.0 com React 19.2.8 e jsdom, e toda saída colada aqui saiu do meu terminal.
Um armário numerado pela ordem, não pelo nome
Imagine um armário em que a primeira gaveta guarda o primeiro hook chamado, a segunda guarda o segundo e assim por diante. As gavetas não têm etiquetas como “estado do pet” ou “efeito da agenda”; o React reencontra cada valor apenas pela posição. Se uma condição faz surgir uma gaveta nova no meio, todo o restante fica desalinhado.
Esse é o motivo técnico da regra de ordem dos Hooks. Cada render precisa chamá-los na mesma sequência. Na primeira reprodução, numere as chamadas antes e depois da condição e localize o primeiro número que muda; a tabela e o stack trace confirmam exatamente onde a ordem deixou de bater.
O componente abaixo tem um defeito de uma linha só. Ele mostra um aviso enquanto nenhum paciente está selecionado e, quando um paciente é escolhido, busca a ficha na API.
import { useState, useEffect } from 'react';
import { buscarFicha } from './api.js';
export function FichaDoPaciente({ pacienteId }) {
const [ficha, setFicha] = useState(null);
if (!pacienteId) {
return <p>Selecione um paciente na agenda.</p>;
}
useEffect(() => {
let ativo = true;
buscarFicha(pacienteId).then((dados) => {
if (ativo) setFicha(dados);
});
return () => {
ativo = false;
};
}, [pacienteId]);
return <p>{ficha ? `${ficha.nome} — ${ficha.peso} kg` : 'Carregando ficha...'}</p>;
}O useEffect está depois do return. Enquanto pacienteId for nulo, esse
useEffect nunca chega a ser chamado. O roteiro que reproduz o problema tem duas
renderizações: primeiro sem paciente, depois com paciente.
import { raiz } from './dom.mjs';
import { act } from 'react';
import { createRoot } from 'react-dom/client';
import { FichaDoPaciente } from './ficha.jsx';
const root = createRoot(raiz);
await act(() => {
root.render(<FichaDoPaciente pacienteId={null} />);
});
console.log('render 1:', raiz.innerHTML);
await act(() => {
root.render(<FichaDoPaciente pacienteId="P-4471" />);
});
console.log('render 2:', raiz.innerHTML);Três coisas dessa saída resolvem quase todo caso na prática.
A primeira é que o aviso vem antes do erro, e é o aviso que tem a informação
útil. A tabela compara a lista de hooks do render anterior com a do render
seguinte, linha a linha. O undefined na coluna da esquerda significa “aqui não
havia hook nenhum”. A fila de acentos circunflexos marca o slot em que a
comparação falhou — no caso, o slot 2.
A segunda é que o erro em si não diz nada além da frase. Não adianta procurar detalhe nele.
A terceira é que a linha do seu código está no meio da pilha, não no topo:
at FichaDoPaciente (/private/tmp/clinica-patas/ficha.jsx:11:3). A linha 11 do
arquivo é exatamente o useEffect. Os quadros acima são todos internos do React.
Cortei a cauda da pilha aqui e nos outros blocos deste artigo: abaixo do último
quadro colado só vem mais função interna do React, nada do seu código.
Por que o React identifica hook por posição, e não por nome
O React não sabe que aquele useState se chama ficha. Para ele, cada
componente montado carrega uma lista encadeada de slots, preenchida na ordem em
que os hooks foram chamados no primeiro render. Nos renders seguintes, ele
percorre a mesma lista na mesma ordem: primeira chamada pega o slot 1, segunda
chamada pega o slot 2, e assim por diante.
Dá para enxergar isso trocando os hooks por um invólucro que registra a posição antes de delegar para o React:
import * as React from 'react';
let slot = 0;
export function abrirRender(componente) {
slot = 0;
console.log(`render de <${componente}>`);
}
export function useState(inicial) {
console.log(` slot ${++slot} -> useState`);
return React.useState(inicial);
}
export function useEffect(efeito, deps) {
console.log(` slot ${++slot} -> useEffect`);
return React.useEffect(efeito, deps);
}Trocando os imports da FichaDoPaciente por esses, acrescentando
abrirRender('FichaDoPaciente') na primeira linha do corpo e rodando o mesmo
roteiro de duas renderizações, os slots aparecem:
O primeiro render fecha com um slot só. O segundo pede dois — e não existe slot 2
para casar. O terceiro bloco não é uma renderização nova da sua aplicação: quando
um render lança, o React refaz aquela mesma renderização uma vez antes de
propagar o erro, e a segunda tentativa bate na mesma parede. E isso não é
instrumentação do modo de desenvolvimento: trocando o act por flushSync e
rodando com NODE_ENV=production, as três passadas continuam aparecendo.
Se você nunca reparou em quantas vezes um componente é renderizado, vale ler por que o componente roda de novo antes de seguir: a lista de hooks é percorrida inteira a cada um desses re-renders.
O par de hooks que troca de lugar sem erro nenhum
Aqui está a prova de que o React só olha para a posição. O card do paciente tem
dois useState, e alguém escreveu a ordem invertida no modo de edição:
import { useState } from 'react';
export function CardPaciente({ modoEdicao }) {
let nome, peso;
if (modoEdicao) {
[peso] = useState(4.2);
[nome] = useState('Nina');
} else {
[nome] = useState('Nina');
[peso] = useState(4.2);
}
return (
<p>
{nome} — {peso} kg
</p>
);
}A contagem não muda: são dois hooks nos dois caminhos. Renderizando primeiro em leitura e depois em edição:
Nenhum erro. Nenhum aviso. O peso virou “Nina” e o nome virou 4.2, porque o slot
1 sempre devolve o que foi guardado no slot 1 — e no render de edição quem leu o
slot 1 foi a variável peso.
O return antecipado que engole o useEffect
Essa é a causa número um, e ela se disfarça de várias formas. Todas têm o mesmo efeito: em algum caminho de execução, uma chamada de hook é pulada.
| forma | o que muda na contagem | o que o React faz |
|---|---|---|
hook depois de um return antecipado |
de N para N+1 quando a condição inverte | Rendered more hooks… |
hook depois de um return dentro de um catch |
cai para zero naquele render | nada — o estado é remontado depois |
hook dentro de um if que embrulha todos os hooks |
zero num render, N no outro | nada — o estado é remontado |
hook dentro de map ou for |
acompanha o tamanho da lista | Rendered more ou fewer hooks… |
| hook dentro de um handler de evento | não roda durante o render | Invalid hook call |
As duas linhas do meio surpreendem quem só conhece a primeira. Veja o cabeçalho da ficha, que só mostra o campo de anotação para quem é veterinário:
import { useState } from 'react';
export function CabecalhoDaFicha({ ehVeterinario }) {
if (ehVeterinario) {
const [rascunho, setRascunho] = useState('');
return (
<textarea
value={rascunho}
onChange={(e) => setRascunho(e.target.value)}
placeholder="Anotação da consulta"
/>
);
}
return <p>Ficha em modo somente leitura.</p>;
}Renderizei em modo veterinário, digitei uma anotação, troquei para somente leitura e voltei:
Nenhum erro — e a anotação sumiu. O motivo é que o render do meio não chamou hook
nenhum, então a lista do componente ficou vazia; no render seguinte o React
tratou o componente como se estivesse montando pela primeira vez e devolveu o
valor inicial. Um if que embrulha todos os hooks não estoura: ele apaga
estado em silêncio.
Hook dentro de laço e hook dentro de callback
Na agenda do dia, o estado “confirmada” foi parar dentro do map:
import { useState } from 'react';
export function AgendaDoDia({ consultas }) {
const [busca] = useState('');
return (
<ul>
{consultas.map((consulta) => {
const [confirmada] = useState(false);
return (
<li key={consulta.id}>
{consulta.paciente} {confirmada ? '(confirmada)' : '(pendente)'} {busca}
</li>
);
})}
</ul>
);
}Com duas consultas funciona. Quando chega a terceira do dia, a contagem sobe de 3 para 4:
Repare no quadro at Array.map (<anonymous>) no meio da pilha: ele é a assinatura
desse caso. Sempre que você vir Array.map entre o seu componente e o hook, o
problema é hook dentro de laço. Se a sua lista veio de uma API, vale conferir
também como renderizar listas com a prop key,
que é o outro erro clássico do mesmo map.
Já o hook dentro de um handler dá um erro diferente. O botão de confirmar consulta:
import { useState } from 'react';
export function BotaoConfirmar({ consulta }) {
function confirmar() {
const [enviando, setEnviando] = useState(false);
if (!enviando) setEnviando(true);
console.log(`confirmando ${consulta}...`);
}
return <button onClick={confirmar}>Confirmar consulta</button>;
}O componente monta sem reclamar. O erro só aparece no clique:
Mensagem diferente porque a causa é diferente: fora do render, o React nem tem
uma lista de hooks aberta para contar. A correção é subir o
useState para o corpo do componente e deixar no handler
só a chamada de setEnviando.
A variação “Rendered fewer hooks than expected”
É o mesmo defeito, com os dois renders na ordem inversa. Voltando à
FichaDoPaciente do começo, agora com paciente primeiro e nulo depois:
await act(() => root.render(<FichaDoPaciente pacienteId="P-4471" />));
console.log('render 1:', raiz.innerHTML);
await act(() => root.render(<FichaDoPaciente pacienteId={null} />));
console.log('render 2:', raiz.innerHTML);A mensagem é mais gentil — ela já sugere o return antecipado. Em compensação,
o diagnóstico é pior: não vem tabela, não vem o nome do componente e não vem
nenhum quadro do seu código na pilha. Faz sentido, porque o erro é detectado no
fim do render, quando o React percebe que sobrou slot na lista, e não no momento
em que um hook faltou.
E existe um caso em que nem esse erro sai: quando o render passa a chamar zero
hooks. É o que acontece no CabecalhoDaFicha da seção anterior. O React só
reclama de hook faltando se pelo menos um hook rodou e sobrou slot depois dele.
A condição escondida dentro de um hook customizado
Este é o caso que mais toma tempo, porque o componente parece impecável. O
hook customizado é que tem o return
antecipado:
import { useState, useEffect } from 'react';
import { buscarFicha } from './api.js';
export function useFichaDoPaciente(pacienteId) {
const [ficha, setFicha] = useState(null);
if (!pacienteId) return null;
useEffect(() => {
buscarFicha(pacienteId).then(setFicha);
}, [pacienteId]);
return ficha;
}import { useFichaDoPaciente } from './use-ficha.js';
export function PainelDaClinica({ pacienteId }) {
const ficha = useFichaDoPaciente(pacienteId);
return <p>{ficha ? ficha.nome : 'Nenhuma ficha aberta.'}</p>;
}O aviso acusa PainelDaClinica, que não tem defeito nenhum — o painel só chama
um hook, uma vez, incondicionalmente. Quem tem o defeito é useFichaDoPaciente,
e ele aparece só na pilha. É por isso que a pilha vale mais que a mensagem: a
lista de hooks pertence ao componente, e tudo que os hooks customizados chamam
entra na mesma lista.
A correção: hooks primeiro, condição depois
A regra prática cabe numa linha: nenhum return antes do último hook. Todos
os hooks ficam no topo do corpo, sempre, e a condição desce para depois deles.
Dentro do efeito, um if normal decide se ele faz alguma coisa:
import { useState, useEffect } from 'react';
import { buscarFicha } from './api.js';
export function FichaDoPaciente({ pacienteId }) {
const [ficha, setFicha] = useState(null);
useEffect(() => {
if (!pacienteId) return;
let ativo = true;
buscarFicha(pacienteId).then((dados) => {
if (ativo) setFicha(dados);
});
return () => {
ativo = false;
};
}, [pacienteId]);
if (!pacienteId) {
return <p>Selecione um paciente na agenda.</p>;
}
return <p>{ficha ? `${ficha.nome} — ${ficha.peso} kg` : 'Carregando ficha...'}</p>;
}Repare que o return antecipado continuou existindo. Ele nunca foi o
problema; o problema era o hook depois dele. Se você quiser aprofundar as formas
de mostrar uma coisa ou outra na tela, veja
renderização condicional no React.
Para o caso do laço, a correção é outra: cada item vira um componente, com a própria lista de hooks.
import { useState } from 'react';
function LinhaDaConsulta({ paciente }) {
const [confirmada, setConfirmada] = useState(false);
return (
<li>
{paciente} {confirmada ? '(confirmada)' : '(pendente)'}
<button onClick={() => setConfirmada(true)}>ok</button>
</li>
);
}
export function AgendaDoDia({ consultas }) {
return (
<ul>
{consultas.map((consulta) => (
<LinhaDaConsulta key={consulta.id} paciente={consulta.paciente} />
))}
</ul>
);
}Confirmei a consulta da Nina e depois acrescentei a terceira consulta do dia:
Agenda cresceu, nada quebrou, e a confirmação da Nina continuou de pé — porque
cada LinhaDaConsulta tem o próprio slot 1.
Em produção o mesmo defeito vira “Minified React error #310”
Se o erro chegou ao ambiente publicado, a mensagem muda de cara. Rodei a versão
quebrada com NODE_ENV=production, que faz o React carregar o build de produção:
A checagem continua lá, mas a tabela comparativa e o texto da mensagem, não —
eles só existem no build de desenvolvimento. Se alguém abrir um chamado dizendo
“deu erro 310 na tela do paciente”, é exatamente este artigo. E veja a última
linha: render 2: saiu vazio. A raiz que tinha um parágrafo ficou sem nada,
porque um erro de render não capturado desmonta a árvore inteira. É a tela branca
que o usuário relata.
O lint que pega isso antes do navegador
Nenhuma das formas da tabela precisa chegar ao navegador. O
eslint-plugin-react-hooks reconhece todas elas por análise estática.
npm i -D eslint eslint-plugin-react-hooks
npx eslint ficha.jsx card.jsx cabecalho.jsx agenda.jsx botao.jsx use-ficha.jsimport reactHooks from 'eslint-plugin-react-hooks';
export default [
{
files: ['**/*.{js,jsx}'],
...reactHooks.configs.flat['recommended-latest'],
languageOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
parserOptions: { ecmaFeatures: { jsx: true } },
},
},
];Rodando nos seis arquivos quebrados deste artigo, com ESLint 10.9.0 e o plugin na versão 7.1.1:
Duas coisas nessa saída valem o preço da instalação. A primeira é que o plugin
pegou justamente os dois arquivos que não geram erro nenhum em tempo de
execução: o card.jsx, com os quatro useState apontados um a um, e o
cabecalho.jsx, que só apaga em silêncio o que a pessoa digitou. Os dois
defeitos mudos deste artigo, que nenhuma renderização denuncia, saem de graça
numa análise que nem executa o código. A segunda é a mensagem do cabecalho.jsx
e do use-ficha.js, mais específica que as outras: ela pergunta se o hook não
teria ficado depois de um return antecipado. Rodando nos dois arquivos
corrigidos, o plugin sai calado, com código de saída zero.
Por onde seguir
Se você chegou aqui por causa de um useEffect fora do lugar, o passo seguinte é
entender bem o ciclo dele: useEffect, dependências e
limpeza explica quando o efeito roda, quando ele roda de
novo e o que a função de limpeza resolve. O parente próximo deste erro é o
Maximum update depth exceeded, que
também nasce de efeito mal posicionado, mas se manifesta como loop em vez de
parada. E, para ver onde hooks entram na ordem de estudo, o
guia de React tem o mapa inteiro.
Prefere aprender em vídeo?
Tem uma aula sobre este assunto no nosso canal.
Perguntas frequentes
Posso usar if dentro do useEffect?
Por que a tela inteira fica em branco quando esse erro acontece?
useState dentro de map nunca funciona?
E se eu realmente precisar de um número variável de estados?
Trocar a ordem de dois useState do mesmo tipo dá erro?
Dúvidas e comentários
Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.
Entrar para perguntarÉ o mesmo login gratuito dos cursos.
Nenhuma dúvida por aqui ainda — a primeira pode ser a sua.
Todo o código deste artigo foi executado em Node 24.16.0, React 19.2.8, jsdom 30.0.1, e as saídas exibidas são as reais — como produzimos este conteúdo.
Fontes consultadas
- React — Rules of Hooks — react.dev
- React — Invalid hook call warning — react.dev
- eslint-plugin-react-hooks no npm — npmjs.com



