Back to Blog
ReactSuspenseData FetchingAsync

Data Fetching with React Suspense

Master data fetching with Suspense. From basic patterns to error boundaries to streaming.

B
Bootspring Team
Engineering
July 31, 2021
6 min read

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.

Share this article

Help spread the word about Bootspring