Isso combina com o seu sintoma?
- O spinner fica girando e a página nunca é renderizada
- O texto 'Loading...' nunca some
- Atualizar não muda nada
- O Console do DevTools (F12) mostra um erro em vermelho, ou a aba Network mostra uma requisição que falhou (em vermelho)
Um spinner infinito quase sempre significa a mesma coisa: os dados nunca terminaram de carregar e a flag de carregamento nunca foi desligada. Existem 4 causas comuns — e a maioria é resolvida com duas linhas de código.
Causa 1 — O carregamento nunca é limpo quando a requisição falha (a mais comum)
Código típico:
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('/api/data')
.then(res => res.json())
.then(data => {
setData(data);
setLoading(false); // only runs on success
});
}, []);
Se a requisição falha (queda de rede, bloqueio de CORS), a execução nunca chega ao .then, então setLoading(false) nunca é chamado e o spinner gira para sempre. Segundo a MDN, o fetch rejeita a promise em erros de rede e URLs inválidas (falhas de CORS também aparecem como erros de rede).
Solução: limpe o carregamento no finally
Envolva a chamada em try / catch / finally para que o bloco finally sempre rode — em caso de sucesso ou de falha. A MDN confirma que finally (e Promise.prototype.finally) roda tanto se a promise for cumprida quanto rejeitada, o que faz dele o lugar certo para limpezas como desligar a flag de carregamento.
useEffect(() => {
const load = async () => {
try {
setLoading(true);
const res = await fetch('/api/data');
if (!res.ok) throw new Error(`HTTP ${res.status}`); // see Cause 2
setData(await res.json());
} catch (e) {
setError(e.message); // show an error state
} finally {
setLoading(false); // always runs → spinner always stops
}
};
load();
}, []);
Está usando uma cadeia de .then()? Adicione .finally(() => setLoading(false)) no final.
> Se for um erro de CORS ('blocked by CORS policy' na aba Network), o finally para o spinner, mas você ainda não vai receber os dados. A solução real é no servidor: a API precisa enviar um cabeçalho Access-Control-Allow-Origin — isso não dá para resolver pelo frontend.
Causa 2 — Um 404/500 é tratado como sucesso
O fetch() não lança erro em status HTTP de erro como 404 ou 500 — a promise é resolvida normalmente. Aí você tenta fazer res.json() de uma página de erro (HTML) e quebra, ou renderiza com dados vazios. MDN: uma promise de fetch não é rejeitada em um 404/504, então você mesmo precisa verificar Response.ok / Response.status.
Solução: sempre verifique res.ok
É a linha if (!res.ok) throw ... acima. Agora um 404 cai no catch, então você vê um estado de erro em vez de um spinner sem fim.
Causa 3 — useEffect entra em loop infinito
Um array de dependências errado faz o componente re-renderizar → refazer o fetch → re-renderizar… e o spinner não para.
Dois erros comuns:
useEffect(() => { /* ... */ }); // 🚩 no array → runs after every render
useEffect(() => { /* ... */ }, [options]); // 🚩 options is an object → new on every render
Documentação do React: objetos, arrays e funções são recriados a cada render, então colocá-los no array de dependências faz o valor ser diferente em cada commit — um loop infinito.
Solução
- Rodar uma vez só → passe um array vazio
[]:useEffect(() => { ... }, []); - Crie objetos/funções dentro do Effect; mantenha só primitivos (strings, números) nas dependências:
useEffect(() => {
const options = { userId, roomId }; // created inside the Effect
// ...
}, [userId, roomId]); // primitives only
- Atualize o estado a partir dele mesmo com a forma de updater:
setCount(c => c + 1)(nãosetCount(count + 1)com[count], que corre o risco de gerar um loop).
Causa 4 — Respostas fora de ordem (troca rápida de aba/busca)
Mudar um input rapidamente pode fazer uma requisição antiga ser resolvida por último e deixar o estado travado. O padrão oficial do React usa uma flag ignore de limpeza:
useEffect(() => {
let ignore = false;
const load = async () => {
try {
const res = await fetch(`/api/data?id=${id}`);
const data = await res.json();
if (!ignore) setData(data);
} finally {
if (!ignore) setLoading(false);
}
};
load();
return () => { ignore = true; }; // cleanup: ignore stale responses
}, [id]);
Checklist de 3 minutos
- F12 → Console: algum erro em vermelho? Essa mensagem é a causa real.
- F12 → Network: alguma requisição que falhou (em vermelho)? É um 404 / 500 / erro de CORS?
- O
setLoading(false)está dentro de um finally? Se não → Causa 1. - Você verifica
res.ok? Se não → Causa 2. - O
useEffecttem um array de dependências correto? Ausente ou um objeto → Causa 3.
> Solução confirmada: as causas 1–3 são as soluções padrão, verificadas na documentação oficial do React e da MDN. A maioria dos spinners infinitos é resolvida com duas linhas — 'limpar o carregamento no finally' + 'verificar res.ok'.
