React Suspense does not detect data fetching that occurs inside useEffect hooks or event handlers. This is a fundamental design limitation because Suspense works by catching promises thrown during the render phase, not from side effects. To fix this, you must use Suspense-compatible data fetching patterns like render-as-you-fetch or libraries with built-in Suspense support.
React Suspense is a mechanism that allows components to "suspend" rendering while waiting for asynchronous operations like data fetching. However, Suspense only works when promises are thrown during the component render phase, not from side effects like useEffect. When you fetch data inside useEffect, the component renders first with initial state, then useEffect runs after rendering to start the data fetch. This "fetch-on-render" pattern is incompatible with Suspense because by the time useEffect runs, the render phase has already completed. Suspense expects components to throw promises during rendering so it can catch them and show fallback content while waiting. This is a fundamental architectural difference between traditional data fetching patterns and the Suspense model.
Recognize that this is not a bug but a fundamental limitation. Suspense requires promises to be thrown during the render phase, while useEffect runs after rendering. You cannot make Suspense work with useEffect-based data fetching without completely changing your approach.
// This will NEVER work with Suspense
function MyComponent() {
const [data, setData] = useState(null);
useEffect(() => {
fetch('/api/data')
.then(res => res.json())
.then(setData);
}, []);
if (!data) return <div>Loading...</div>; // Suspense won't catch this
return <div>{data.title}</div>;
}The easiest solution is to use a library that has built-in Suspense support. Libraries like React Query, SWR, and Apollo Client have Suspense modes that handle promise throwing for you.
// React Query with Suspense
import { useSuspenseQuery } from '@tanstack/react-query';
function MyComponent() {
// This works with Suspense - no loading state needed
const { data } = useSuspenseQuery({
queryKey: ['data'],
queryFn: () => fetch('/api/data').then(res => res.json())
});
return <div>{data.title}</div>;
}
// Wrap with Suspense boundary
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<MyComponent />
</Suspense>
);
}If you cannot use a library, you can implement Suspense-compatible data fetching by creating a resource that throws promises during rendering. This requires wrapping your fetch call and managing promise states.
// Create a suspense-compatible resource
function wrapPromise<T>(promise: Promise<T>) {
let status = 'pending';
let result: T;
let suspender = promise.then(
(r) => {
status = 'success';
result = r;
},
(e) => {
status = 'error';
result = e;
}
);
return {
read() {
if (status === 'pending') {
throw suspender; // Suspense catches this
} else if (status === 'error') {
throw result;
}
return result;
}
};
}
// Start fetching BEFORE rendering
const resource = wrapPromise(fetch('/api/data').then(r => r.json()));
function MyComponent() {
const data = resource.read(); // Throws promise if pending
return <div>{data.title}</div>;
}If you are using Next.js with the App Router, Server Components can use async/await directly and work seamlessly with Suspense without any special setup.
// app/page.tsx - Server Component
async function MyComponent() {
// This automatically works with Suspense
const data = await fetch('https://api.example.com/data').then(r => r.json());
return <div>{data.title}</div>;
}
export default function Page() {
return (
<Suspense fallback={<div>Loading...</div>}>
<MyComponent />
</Suspense>
);
}If you have existing code using useEffect, refactor incrementally. Start by identifying components that fetch data on mount, then replace them one at a time with Suspense-compatible alternatives. Keep backward compatibility by maintaining both patterns during migration.
// Before: useEffect pattern
function OldComponent() {
const [data, setData] = useState(null);
useEffect(() => {
fetchData().then(setData);
}, []);
return data ? <Content data={data} /> : <Loading />;
}
// After: Suspense pattern
function NewComponent() {
const data = useSuspenseQuery({ queryKey: ['data'], queryFn: fetchData });
return <Content data={data} />;
}
// During migration, use feature flags or gradual rollout
function Component() {
const useSuspense = useFeatureFlag('suspense-enabled');
return useSuspense ? <NewComponent /> : <OldComponent />;
}When using Suspense for data fetching, errors during fetch operations need to be caught by Error Boundaries. Always wrap Suspense components with an Error Boundary to handle failures gracefully.
import { ErrorBoundary } from 'react-error-boundary';
function App() {
return (
<ErrorBoundary fallback={<div>Something went wrong</div>}>
<Suspense fallback={<div>Loading...</div>}>
<MyComponent />
</Suspense>
</ErrorBoundary>
);
}The Suspense model represents a paradigm shift from imperative data fetching (useEffect) to declarative data fetching (render-as-you-fetch). In the traditional model, you control when fetching happens through effects. With Suspense, components declare what data they need, and the fetching happens automatically during render. This enables better performance through request parallelization and eliminates waterfall problems where child components wait for parent data. The official React documentation notes that Suspense-enabled data fetching without an opinionated framework is still experimental, and the API for integrating custom data sources with Suspense remains unstable. For production use, it is strongly recommended to use established libraries (React Query, SWR, Relay) or frameworks (Next.js, Remix) that have stable Suspense integrations. When implementing custom Suspense resources, be aware of cache invalidation and refetching patterns - you need to manage when to create new resource instances versus reusing cached ones. The wrapPromise pattern shown above is simplified for demonstration; production implementations need proper error handling, cache management, and request deduplication.
React Hook useCallback has a missing dependency: 'variable'. Either include it or remove the dependency array react-hooks/exhaustive-deps
React Hook useCallback has a missing dependency
Cannot use private fields in class components without TS support
Cannot use private fields in class components without TS support
Cannot destructure property 'xxx' of 'undefined'
Cannot destructure property of undefined when accessing props
useNavigate() may be used only in the context of a <Router> component.
useNavigate() may be used only in the context of a Router component
Cannot find module or its corresponding type declarations
How to fix "Cannot find module or type declarations" in Vite