--- title: React 19 Suspense for Data Fetching Pattern Reference --- # React 19 Suspense for Data Fetching Pattern Reference React 19's new Suspense integration for **data fetching** is a preview feature that allows components to suspend (pause rendering) until data is available, without `useEffect + state`. **Important:** This is a **preview** it requires specific setup and is not yet stable for production, but you should know the pattern for React 19 migration planning. --- ## What Changed in React 19? React 18 Suspense only supported **code splitting** (lazy components). React 19 extends it to **data fetching** if certain conditions are met: - **Lib usage** the data fetching library must implement Suspense (e.g., React Query 5+, SWR, Remix loaders) - **Or your own promise tracking** wrap promises in a way React can track their suspension - **No more "no hook after suspense"** you can use Suspense directly in components with `use()` --- ## React 18 Suspense (Code Splitting Only) ```jsx // React 18 Suspense for lazy imports only: const LazyComponent = React.lazy(() => import('./Component')); function App() { return ( }> ); } ``` Trying to suspend for data in React 18 required hacks or libraries: ```jsx // React 18 hack not recommended: const dataPromise = fetchData(); const resource = { read: () => { throw dataPromise; // Throw to suspend } }; function Component() { const data = resource.read(); // Throws promise → Suspense catches it return
{data}
; } ``` --- ## React 19 Suspense for Data Fetching (Preview) React 19 provides **first-class support** for Suspense with promises via the `use()` hook: ```jsx // React 19 Suspense for data fetching: function UserProfile({ userId }) { const user = use(fetchUser(userId)); // Suspends if promise pending return
{user.name}
; } function App() { return ( }> ); } ``` **Key differences from React 18:** - `use()` unwraps the promise, component suspends automatically - No need for `useEffect + state` trick - Cleaner code, less boilerplate --- ## Pattern 1: Simple Promise Suspense ```jsx // Raw promise (not recommended in production): function DataComponent() { const data = use(fetch('/api/data').then(r => r.json())); return
{JSON.stringify(data, null, 2)}
; } function App() { return ( }> ); } ``` **Problem:** Promise is recreated every render. Solution: wrap in `useMemo`. --- ## Pattern 2: Memoized Promise (Better) ```jsx function DataComponent({ id }) { // Only create promise once per id: const dataPromise = useMemo(() => fetch(`/api/data/${id}`).then(r => r.json()), [id] ); const data = use(dataPromise); return
{JSON.stringify(data, null, 2)}
; } function App() { const [id, setId] = useState(1); return ( }> ); } ``` --- ## Pattern 3: Library Integration (React Query) Modern data libraries support Suspense directly. React Query 5+ example: ```jsx // React Query 5+ with Suspense: import { useSuspenseQuery } from '@tanstack/react-query'; function UserProfile({ userId }) { // useSuspenseQuery throws promise if suspended const { data: user } = useSuspenseQuery({ queryKey: ['user', userId], queryFn: () => fetchUser(userId), }); return
{user.name}
; } function App() { return ( }> ); } ``` **Advantage:** Library handles caching, retries, and cache invalidation. --- ## Pattern 4: Error Boundary Integration Combine Suspense with Error Boundary to handle both loading and errors: ```jsx function UserProfile({ userId }) { const user = use(fetchUser(userId)); // Suspends while loading return
{user.name}
; } function App() { return ( }> }> ); } class ErrorBoundary extends React.Component { state = { error: null }; static getDerivedStateFromError(error) { return { error }; } render() { if (this.state.error) return this.props.fallback; return this.props.children; } } ``` --- ## Nested Suspense Boundaries Use multiple Suspense boundaries to show partial UI while waiting for different data: ```jsx function App({ userId }) { return (
}> }>
); } function UserProfile({ userId }) { const user = use(fetchUser(userId)); return

{user.name}

; } function UserPosts({ userId }) { const posts = use(fetchUserPosts(userId)); return ; } ``` Now: - User profile shows spinner while loading - Posts show spinner independently - Both can render as they complete --- ## Sequential vs Parallel Suspense ### Sequential (wait for first before fetching second) ```jsx function App({ userId }) { const user = use(fetchUser(userId)); // Must complete first return ( }> {/* Depends on user */} ); } function UserPosts({ userId }) { const posts = use(fetchUserPosts(userId)); return ; } ``` ### Parallel (fetch both at once) ```jsx function App({ userId }) { return (
}> }> {/* Fetches in parallel */}
); } ``` --- ## Migration Strategy for React 18 → React 19 ### Phase 1 No changes required Suspense is still optional and experimental for data fetching. All existing `useEffect + state` patterns continue to work. ### Phase 2 Wait for stability Before adopting Suspense data fetching in production: - Wait for React 19 to ship (not preview) - Verify your data library supports Suspense - Plan migration after app stabilizes on React 19 core ### Phase 3 Refactor to Suspense (optional, post-preview) Once stable, profile candidates: ```bash grep -rn "useEffect.*fetch\|useEffect.*axios\|useEffect.*graphql" src/ --include="*.js" --include="*.jsx" ``` ```jsx // Before (React 18): function UserProfile({ userId }) { const [user, setUser] = useState(null); useEffect(() => { fetchUser(userId).then(setUser); }, [userId]); if (!user) return ; return
{user.name}
; } // After (React 19 with Suspense): function UserProfile({ userId }) { const user = use(fetchUser(userId)); return
{user.name}
; } // Must be wrapped in Suspense: }> ``` --- ## Important Warnings 1. **Still Preview** Suspense for data is marked experimental, behavior may change 2. **Performance** promises are recreated on every render without memoization; use `useMemo` 3. **Cache** `use()` doesn't cache; use React Query or similar for production apps 4. **SSR** Suspense SSR support is limited; check Next.js version requirements