React Suspense simplifies async data handling. Here's how to use it for data fetching.
Basic Suspense#
1import { Suspense } from 'react';
2
3// Suspense catches loading state
4function App() {
5 return (
6 <Suspense fallback={<Loading />}>
7 <UserProfile userId="1" />
8 </Suspense>
9 );
10}
11
12function Loading() {
13 return <div className="spinner">Loading...</div>;
14}
15
16// The component that fetches data
17function UserProfile({ userId }: { userId: string }) {
18 const user = use(fetchUser(userId));
19
20 return (
21 <div>
22 <h1>{user.name}</h1>
23 <p>{user.email}</p>
24 </div>
25 );
26}The use Hook (React 19+)#
1import { use } from 'react';
2
3// use() unwraps promises during render
4function UserProfile({ userId }: { userId: string }) {
5 const user = use(fetchUser(userId));
6
7 return <div>{user.name}</div>;
8}
9
10// Can be called conditionally (unlike hooks)
11function ConditionalData({ shouldFetch }: { shouldFetch: boolean }) {
12 if (!shouldFetch) {
13 return <div>No data</div>;
14 }
15
16 const data = use(fetchData());
17 return <div>{data.value}</div>;
18}
19
20// Works with Context too
21function ThemedComponent() {
22 const theme = use(ThemeContext);
23 return <div style={{ color: theme.primary }}>Themed</div>;
24}Resource Pattern#
1// Create a resource (cache for promise)
2function createResource<T>(promise: Promise<T>) {
3 let status: 'pending' | 'success' | 'error' = 'pending';
4 let result: T;
5 let error: Error;
6
7 const suspender = promise.then(
8 (data) => {
9 status = 'success';
10 result = data;
11 },
12 (err) => {
13 status = 'error';
14 error = err;
15 }
16 );
17
18 return {
19 read(): T {
20 switch (status) {
21 case 'pending':
22 throw suspender; // Suspense catches this
23 case 'error':
24 throw error; // Error boundary catches this
25 case 'success':
26 return result;
27 }
28 },
29 };
30}
31
32// Usage
33const userResource = createResource(fetchUser('1'));
34
35function UserProfile() {
36 const user = userResource.read(); // Suspends if pending
37 return <div>{user.name}</div>;
38}
39
40function App() {
41 return (
42 <ErrorBoundary fallback={<Error />}>
43 <Suspense fallback={<Loading />}>
44 <UserProfile />
45 </Suspense>
46 </ErrorBoundary>
47 );
48}Error Boundaries#
1import { Component, ReactNode } from 'react';
2
3interface Props {
4 children: ReactNode;
5 fallback: ReactNode | ((error: Error, reset: () => void) => ReactNode);
6}
7
8interface State {
9 hasError: boolean;
10 error: Error | null;
11}
12
13class ErrorBoundary extends Component<Props, State> {
14 state: State = { hasError: false, error: null };
15
16 static getDerivedStateFromError(error: Error): State {
17 return { hasError: true, error };
18 }
19
20 componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
21 console.error('Error caught:', error, errorInfo);
22 }
23
24 reset = () => {
25 this.setState({ hasError: false, error: null });
26 };
27
28 render() {
29 if (this.state.hasError) {
30 if (typeof this.props.fallback === 'function') {
31 return this.props.fallback(this.state.error!, this.reset);
32 }
33 return this.props.fallback;
34 }
35
36 return this.props.children;
37 }
38}
39
40// Usage with reset
41function App() {
42 return (
43 <ErrorBoundary
44 fallback={(error, reset) => (
45 <div>
46 <p>Error: {error.message}</p>
47 <button onClick={reset}>Try Again</button>
48 </div>
49 )}
50 >
51 <Suspense fallback={<Loading />}>
52 <DataComponent />
53 </Suspense>
54 </ErrorBoundary>
55 );
56}Nested Suspense#
1// Multiple suspense boundaries for granular loading
2function Dashboard() {
3 return (
4 <div className="dashboard">
5 <Suspense fallback={<HeaderSkeleton />}>
6 <Header />
7 </Suspense>
8
9 <div className="content">
10 <Suspense fallback={<SidebarSkeleton />}>
11 <Sidebar />
12 </Suspense>
13
14 <Suspense fallback={<MainSkeleton />}>
15 <MainContent />
16 </Suspense>
17 </div>
18 </div>
19 );
20}
21
22// Each section loads independently
23function Header() {
24 const user = use(fetchCurrentUser());
25 return <header>Welcome, {user.name}</header>;
26}
27
28function Sidebar() {
29 const nav = use(fetchNavigation());
30 return <nav>{/* nav items */}</nav>;
31}
32
33function MainContent() {
34 const data = use(fetchDashboardData());
35 return <main>{/* dashboard content */}</main>;
36}Streaming with Suspense#
1// Server Components with streaming
2// app/page.tsx
3import { Suspense } from 'react';
4
5async function SlowComponent() {
6 const data = await fetchSlowData(); // 3 seconds
7 return <div>{data.value}</div>;
8}
9
10async function FastComponent() {
11 const data = await fetchFastData(); // 100ms
12 return <div>{data.value}</div>;
13}
14
15export default function Page() {
16 return (
17 <div>
18 {/* Fast content streams first */}
19 <Suspense fallback={<p>Loading fast...</p>}>
20 <FastComponent />
21 </Suspense>
22
23 {/* Slow content streams when ready */}
24 <Suspense fallback={<p>Loading slow...</p>}>
25 <SlowComponent />
26 </Suspense>
27 </div>
28 );
29}React Query with Suspense#
1import { useSuspenseQuery, useSuspenseQueries } from '@tanstack/react-query';
2
3function UserProfile({ userId }: { userId: string }) {
4 const { data: user } = useSuspenseQuery({
5 queryKey: ['user', userId],
6 queryFn: () => fetchUser(userId),
7 });
8
9 return <div>{user.name}</div>;
10}
11
12// Multiple queries
13function UserWithPosts({ userId }: { userId: string }) {
14 const [{ data: user }, { data: posts }] = useSuspenseQueries({
15 queries: [
16 {
17 queryKey: ['user', userId],
18 queryFn: () => fetchUser(userId),
19 },
20 {
21 queryKey: ['posts', userId],
22 queryFn: () => fetchUserPosts(userId),
23 },
24 ],
25 });
26
27 return (
28 <div>
29 <h1>{user.name}</h1>
30 <ul>
31 {posts.map((post) => (
32 <li key={post.id}>{post.title}</li>
33 ))}
34 </ul>
35 </div>
36 );
37}
38
39// Wrap with Suspense
40function App() {
41 return (
42 <QueryClientProvider client={queryClient}>
43 <Suspense fallback={<Loading />}>
44 <UserProfile userId="1" />
45 </Suspense>
46 </QueryClientProvider>
47 );
48}SWR with Suspense#
1import useSWR from 'swr';
2
3function UserProfile({ userId }: { userId: string }) {
4 const { data: user } = useSWR(
5 `/api/users/${userId}`,
6 fetcher,
7 { suspense: true }
8 );
9
10 return <div>{user.name}</div>;
11}
12
13// Global config
14function App() {
15 return (
16 <SWRConfig value={{ suspense: true }}>
17 <Suspense fallback={<Loading />}>
18 <UserProfile userId="1" />
19 </Suspense>
20 </SWRConfig>
21 );
22}Transitions#
1import { useTransition, useState, Suspense } from 'react';
2
3function TabContainer() {
4 const [tab, setTab] = useState('home');
5 const [isPending, startTransition] = useTransition();
6
7 function selectTab(nextTab: string) {
8 startTransition(() => {
9 setTab(nextTab);
10 });
11 }
12
13 return (
14 <div>
15 <nav>
16 <button onClick={() => selectTab('home')}>Home</button>
17 <button onClick={() => selectTab('posts')}>Posts</button>
18 <button onClick={() => selectTab('settings')}>Settings</button>
19 </nav>
20
21 <div className={isPending ? 'opacity-50' : ''}>
22 <Suspense fallback={<TabSkeleton />}>
23 {tab === 'home' && <HomeTab />}
24 {tab === 'posts' && <PostsTab />}
25 {tab === 'settings' && <SettingsTab />}
26 </Suspense>
27 </div>
28 </div>
29 );
30}Preloading Data#
1// Preload on hover
2function UserLink({ userId }: { userId: string }) {
3 const queryClient = useQueryClient();
4
5 const prefetch = () => {
6 queryClient.prefetchQuery({
7 queryKey: ['user', userId],
8 queryFn: () => fetchUser(userId),
9 });
10 };
11
12 return (
13 <Link
14 href={`/users/${userId}`}
15 onMouseEnter={prefetch}
16 onFocus={prefetch}
17 >
18 View User
19 </Link>
20 );
21}
22
23// Preload before navigation
24function Navigation() {
25 const router = useRouter();
26
27 const navigateToUser = async (userId: string) => {
28 // Start fetching
29 const userPromise = fetchUser(userId);
30
31 // Navigate immediately
32 router.push(`/users/${userId}`);
33
34 // Data is ready when component renders
35 };
36
37 return (
38 <button onClick={() => navigateToUser('1')}>
39 Go to User
40 </button>
41 );
42}Best Practices#
Loading States:
✓ Use skeleton components
✓ Place Suspense boundaries strategically
✓ Show meaningful loading states
✓ Avoid layout shift
Error Handling:
✓ Wrap Suspense with ErrorBoundary
✓ Provide retry mechanisms
✓ Show helpful error messages
✓ Log errors for debugging
Performance:
✓ Preload data on hover
✓ Use transitions for tab changes
✓ Cache data appropriately
✓ Stream server components
Conclusion#
React Suspense transforms async data handling with declarative loading states. Use the use hook for simple cases, React Query or SWR for advanced caching, and ErrorBoundary for error handling. Place Suspense boundaries strategically for optimal user experience.