{
  "$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)"
}