The Problem
React Suspense is designed to show a fallback UI while async data loads. But sometimes the fallback never goes away -- the loading spinner spins forever, and the actual content never appears.
Symptoms
- Loading spinner displays indefinitely
- No error message in console
- Network tab shows the request completed successfully
- The component tree shows Suspense fallback is still active
- Clicking away and back does not resolve the issue
The Root Cause
Suspense works by catching promises thrown during render. If the promise never resolves, or resolves to something unexpected, Suspense stays in the fallback state forever.
Real Error Scenario
```javascript // BAD: Promise resolves but Suspense doesn't know about it const resource = fetchData();
function Profile() { const user = resource.read(); // Throws promise return <div>{user.name}</div>; }
function App() { return ( <Suspense fallback={<Loading />}> <Profile /> </Suspense> ); } ```
The resource pattern requires a specific implementation. A naive promise wrapper will not work with Suspense.
Common Cause: Incorrect Resource Wrapper
```javascript // WRONG: Just returning a promise doesn't work with Suspense function createResource(promise) { return promise; // Suspense expects a specific API }
// CORRECT: Implement the Suspense resource pattern function createResource(promise) { let status = 'pending'; let result;
const suspender = promise.then( data => { status = 'success'; result = data; }, error => { status = 'error'; result = error; } );
return { read() { if (status === 'pending') throw suspender; if (status === 'error') throw result; return result; } }; } ```
Fix with React 18 use Hook
```javascript import { use } from 'react';
function Profile({ userId }) {
const userPromise = fetch(/api/users/${userId}).then(r => r.json());
const user = use(userPromise); // React 18 built-in Suspense support
return <div>{user.name}</div>;
}
function App() { return ( <Suspense fallback={<Loading />}> <Profile userId={1} /> </Suspense> ); } ```
Fix: Handle Promise Rejection
function App() {
return (
<ErrorBoundary fallback={<ErrorScreen />}>
<Suspense fallback={<Loading />}>
<Profile userId={1} />
</Suspense>
</ErrorBoundary>
);
}If the promise rejects and there's no Error Boundary, Suspense may stay stuck. Always wrap Suspense with an Error Boundary.
Debugging Steps
- 1.Open DevTools Console and check for unhandled promise rejections
- 2.Check the Network tab -- did the request actually complete?
- 3.Add
console.loginside the promise resolution to verify it fires - 4.Verify the resource wrapper implements the
read()pattern correctly - 5.Check that Suspense wraps the component that throws, not a parent