Does this match your symptom?
- A spinner keeps turning and the page never renders
- The 'Loading...' text never disappears
- Refreshing changes nothing
- DevTools (F12) Console shows a red error, or the Network tab shows a failed (red) request
An infinite spinner almost always means the same thing: the data never finished loading, and the loading flag was never turned off. There are 4 common causes — and most are fixed by two lines of code.
Cause 1 — Loading is never cleared when the request fails (most common)
Typical code:
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('/api/data')
.then(res => res.json())
.then(data => {
setData(data);
setLoading(false); // only runs on success
});
}, []);
If the request fails (network drop, CORS block), execution never reaches .then, so setLoading(false) is never called and the spinner runs forever. Per MDN, fetch rejects its promise on network errors and bad URLs (CORS failures show up as network errors too).
Fix: clear loading in finally
Wrap the call in try / catch / finally so the finally block always runs — success or failure. MDN confirms finally (and Promise.prototype.finally) runs whether the promise is fulfilled or rejected, which makes it the right home for cleanup like clearing a loading flag.
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();
}, []);
Using a .then() chain? Add .finally(() => setLoading(false)) at the end.
> If it's a CORS error ('blocked by CORS policy' in the Network tab), finally stops the spinner but you still won't get the data. The real fix is server-side: the API must send an Access-Control-Allow-Origin header — you can't fix this from the frontend.
Cause 2 — A 404/500 is treated as success
fetch() does not throw on HTTP error statuses like 404 or 500 — the promise resolves normally. You then try to res.json() an error page (HTML) and crash, or render with empty data. MDN: a fetch promise does not reject for 404/504, so you must check Response.ok / Response.status yourself.
Fix: always check res.ok
That's the if (!res.ok) throw ... line above. Now a 404 lands in catch, so you see an error state instead of an endless spinner.
Cause 3 — useEffect loops forever
A wrong dependency array makes the component re-render → re-fetch → re-render… and the spinner never stops.
Two common mistakes:
useEffect(() => { /* ... */ }); // 🚩 no array → runs after every render
useEffect(() => { /* ... */ }, [options]); // 🚩 options is an object → new on every render
React docs: objects, arrays, and functions are recreated on every render, so putting them in the dependency array makes the value different on every commit — an infinite loop.
Fix
- Run once → pass an empty array
[]:useEffect(() => { ... }, []); - Create objects/functions inside the Effect; keep only primitives (strings, numbers) in deps:
useEffect(() => {
const options = { userId, roomId }; // created inside the Effect
// ...
}, [userId, roomId]); // primitives only
- Update state from itself with the updater form:
setCount(c => c + 1)(notsetCount(count + 1)with[count], which risks a loop).
Cause 4 — Out-of-order responses (fast tab/search switching)
Rapidly changing an input can let an old request resolve last and leave state stuck. The official React pattern uses a cleanup ignore flag:
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]);
3-minute checklist
- F12 → Console: any red error? That message is the real cause.
- F12 → Network: any failed (red) request? Is it 404 / 500 / a CORS error?
- Is
setLoading(false)inside a finally? If not → Cause 1. - Do you check
res.ok? If not → Cause 2. - Does
useEffecthave a correct dependency array? Missing or an object → Cause 3.
> Confirmed fix: Causes 1–3 are the standard fixes, verified against the official React and MDN docs. Most infinite spinners are solved by two lines — 'clear loading in finally' + 'check res.ok'.
