{
"$type": "site.standard.document",
"bskyPostRef": {
"cid": "bafyreianhpdmxm3jsc6e4gtgae67pmbxeouehroip7uajrd3pazq66tkz4",
"uri": "at://did:plc:25rdn5elo5izoxrmtis34zuk/app.bsky.feed.post/3mpgginud67x2"
},
"coverImage": {
"$type": "blob",
"ref": {
"$link": "bafkreifq7khknd7i27o562x6alqjr7wxzp7lr6zsrtnr3tkzzdu6h6pvhi"
},
"mimeType": "image/webp",
"size": 75740
},
"path": "/stacknotice/react-suspense-the-use-hook-complete-guide-2026-4442",
"publishedAt": "2026-06-29T11:19:40.000Z",
"site": "https://dev.to",
"tags": [
"react",
"javascript",
"nextjs",
"webdev",
"stacknotice.com/blog/react-suspense-use-hook-2026"
],
"textContent": "Suspense has been in React since 16.6, but data fetching with Suspense was experimental until React 18/19. Now it's a first-class async UI primitive, and the new `use()` hook gives you a direct way to integrate it without a library.\n\n## What Suspense Actually Does\n\nSuspense doesn't know anything about fetching. When a component throws a Promise during render, React catches it, renders the nearest `<Suspense>` boundary's fallback, and retries when the Promise resolves.\n\n\n\n // Conceptually, what libraries do:\n function fetchUser(id: string) {\n if (cache.has(id)) return cache.get(id)\n const promise = fetch(`/api/users/${id}`).then(r => r.json())\n cache.set(id, promise)\n throw promise // ← Suspense catches this\n }\n\n\n## The use() Hook\n\nReact 19's `use()` unwraps a Promise (or Context) inline during render, integrating with Suspense automatically:\n\n\n\n import { use, Suspense } from 'react'\n\n function UserProfile({ userPromise }: { userPromise: Promise<User> }) {\n const user = use(userPromise) // suspends until resolved\n return <div><h2>{user.name}</h2><p>{user.email}</p></div>\n }\n\n function Page() {\n const userPromise = fetchUser('123') // created once, passed down\n\n return (\n <Suspense fallback={<ProfileSkeleton />}>\n <UserProfile userPromise={userPromise} />\n </Suspense>\n )\n }\n\n\n**Critical:** Create the Promise outside the component. If you create it inside, you get a new Promise on every render — infinite suspense loop:\n\n\n\n // ❌ New Promise every render — infinite loop\n function UserProfile({ id }: { id: string }) {\n const user = use(fetchUser(id))\n }\n\n // ✅ Stable Promise reference passed as prop\n function Page({ id }: { id: string }) {\n const userPromise = useMemo(() => fetchUser(id), [id])\n return (\n <Suspense fallback={<Skeleton />}>\n <UserProfile userPromise={userPromise} />\n </Suspense>\n )\n }\n\n\n### use() with Context\n\n`use()` also works as a context reader — unlike `useContext`, it can be called inside conditions:\n\n\n\n function Button({ disabled }: { disabled?: boolean }) {\n if (disabled) return <button disabled>...</button>\n\n const theme = use(ThemeContext) // ✅ conditional call works\n return <button className={theme.buttonClass}>...</button>\n }\n\n\n## Error Boundaries\n\nWhen a Promise rejects, you need an Error Boundary (Suspense only handles pending state):\n\n\n\n npm install react-error-boundary\n\n\n\n import { ErrorBoundary } from 'react-error-boundary'\n\n function FallbackUI({ error, resetErrorBoundary }: FallbackProps) {\n return (\n <div>\n <p>Failed to load: {error.message}</p>\n <button onClick={resetErrorBoundary}>Try again</button>\n </div>\n )\n }\n\n // ErrorBoundary MUST be outside Suspense\n <ErrorBoundary FallbackComponent={FallbackUI}>\n <Suspense fallback={<Loading />}>\n <AsyncComponent />\n </Suspense>\n </ErrorBoundary>\n\n\n## Nested Suspense: Progressive Loading\n\nInstead of one giant loading state, nest boundaries so each section loads independently:\n\n\n\n export default function DashboardPage() {\n const statsPromise = fetchStats() // fast\n const activityPromise = fetchActivity() // medium\n const recsPromise = fetchRecommendations() // slow\n\n return (\n <main>\n <Suspense fallback={<StatsSkeleton />}>\n <StatsSection statsPromise={statsPromise} />\n </Suspense>\n\n <Suspense fallback={<ActivitySkeleton />}>\n <ActivityFeed activityPromise={activityPromise} />\n </Suspense>\n\n <Suspense fallback={<RecsSkeleton />}>\n <Recommendations recsPromise={recsPromise} />\n </Suspense>\n </main>\n )\n }\n\n\nUsers see content progressively as each section resolves — no waiting for the slowest section.\n\n## Suspense in Next.js Server Components\n\nServer Components can be `async` — they await data directly. Suspense coordinates streaming:\n\n\n\n // Server Component — no 'use client'\n async function StatsSection() {\n const stats = await fetchStats()\n return <StatsGrid stats={stats} />\n }\n\n export default function DashboardPage() {\n return (\n <main>\n {/* HTML streams to the client as each section completes */}\n <Suspense fallback={<StatsSkeleton />}>\n <StatsSection />\n </Suspense>\n </main>\n )\n }\n\n\nNo client-side JavaScript needed, no `useEffect`, no re-fetching on hydration.\n\n### Pass Promises from Server to Client\n\nKick off fetching on the server, pass the Promise to a Client Component:\n\n\n\n // Server Component — starts fetch immediately, doesn't await\n export default async function ProductPage({ params }: { params: { id: string } }) {\n const reviewsPromise = fetchReviews(params.id) // no await\n\n return (\n <div>\n <ProductInfo id={params.id} />\n <Suspense fallback={<ReviewsSkeleton />}>\n <ReviewsSection reviewsPromise={reviewsPromise} />\n </Suspense>\n </div>\n )\n }\n\n // Client Component\n 'use client'\n function ReviewsSection({ reviewsPromise }: { reviewsPromise: Promise<Review[]> }) {\n const reviews = use(reviewsPromise)\n return <ReviewsList reviews={reviews} />\n }\n\n\nThe fetch starts on the server before the client hydrates. By the time hydration happens, the Promise may already be resolved.\n\n## useTransition + Suspense: Avoid Fallback Flicker\n\nWhen navigating or updating state, `useTransition` keeps the old content visible instead of showing the skeleton:\n\n\n\n 'use client'\n\n function ProductList() {\n const [isPending, startTransition] = useTransition()\n const [category, setCategory] = useState('all')\n\n function handleChange(newCategory: string) {\n startTransition(() => setCategory(newCategory))\n }\n\n return (\n <div>\n <CategoryFilter onChange={handleChange} disabled={isPending} />\n {/* No skeleton flash — old content stays visible while new loads */}\n <Suspense fallback={<ProductsSkeleton />}>\n <Products category={category} />\n </Suspense>\n </div>\n )\n }\n\n\nWithout `startTransition`: change → skeleton flash → new content.\nWith `startTransition`: change → old content stays (pending) → new content appears.\n\n## Common Mistakes\n\n\n // ❌ ErrorBoundary inside Suspense — rejections bypass it\n <Suspense fallback={<Skeleton />}>\n <ErrorBoundary fallback={<Error />}> {/* wrong order */}\n\n // ✅ ErrorBoundary outside Suspense\n <ErrorBoundary fallback={<Error />}>\n <Suspense fallback={<Skeleton />}>\n\n // ❌ No Suspense boundary around use()\n function Page() {\n return <UserProfile userPromise={promise} /> // throws: no fallback specified\n\n // ✅\n function Page() {\n return (\n <Suspense fallback={<Skeleton />}>\n <UserProfile userPromise={promise} />\n </Suspense>\n )\n }\n\n\n## Quick Reference\n\n\n // use() for Promises\n const data = use(dataPromise)\n\n // use() for Context (works in conditions)\n const theme = use(ThemeContext)\n\n // Standard pattern\n <Suspense fallback={<Loading />}>\n <AsyncComponent />\n </Suspense>\n\n // With error handling\n <ErrorBoundary FallbackComponent={ErrorFallback}>\n <Suspense fallback={<Loading />}>\n <AsyncComponent />\n </Suspense>\n </ErrorBoundary>\n\n // Avoid skeleton flash on navigation\n startTransition(() => setState(newValue))\n\n // Server Component\n async function ServerComp() {\n const data = await fetchData()\n return <div>{data.title}</div>\n }\n\n\nSuspense and `use()` answer one question: \"is this part of the UI ready to render?\" If not, show the fallback. When it is, swap in the real content. The data layer plugs into this mechanism — the coordination logic stays in React.\n\nFull article at stacknotice.com/blog/react-suspense-use-hook-2026",
"title": "React Suspense & the use() Hook: Complete Guide (2026)"
}