to the nearest <Suspense> boundary, and resumes only when the Promise resolves. Rejections are automatically caught by the nearest Error Boundary. The hook does not fetch data; it unwraps a Promise created elsewhere.
Architecture Decisions & Rationale
- Promise Creation Outside the Consumer:
use() expects a stable Promise reference. Creating a new Promise on every render triggers infinite suspension loops. The Promise must be instantiated in a parent component, a Server Component, or memoized via useMemo/React Cache.
- Boundary-Driven Error & Loading States: Loading and error UI are not component concerns. They are structural concerns defined at the route or layout level. This prevents UI flicker and ensures consistent fallback experiences.
- Server Component Integration: In React Server Components, Promises can be created during the initial render pass and passed down as props. Since Server Components do not re-render on the client, the Promise reference remains stable, eliminating the need for client-side memoization.
Implementation Example
Below is a production-ready pattern using an order dashboard. The Server Component initiates the request and defines boundaries. The Client Component consumes the resolved data.
// server/order-page.tsx
import { Suspense } from "react";
import { ErrorBoundary } from "react-error-boundary";
import { OrderDetails } from "@/components/order-details";
import { resolveOrderData } from "@/lib/api/orders";
import { OrderFallback } from "@/components/order-fallback";
import { OrderError } from "@/components/order-error";
export default async function OrderPage({ params }: { params: { orderId: string } }) {
const orderPromise = resolveOrderData(params.orderId);
return (
<ErrorBoundary fallbackRender={({ error }) => <OrderError message={error.message} />}>
<Suspense fallback={<OrderFallback />}>
<OrderDetails orderPromise={orderPromise} />
</Suspense>
</ErrorBoundary>
);
}
// components/order-details.tsx
"use client";
import { use } from "react";
import type { Order } from "@/types/order";
interface OrderDetailsProps {
orderPromise: Promise<Order>;
}
export function OrderDetails({ orderPromise }: OrderDetailsProps) {
const order = use(orderPromise);
return (
<article className="order-card">
<header>
<h2>Order #{order.id}</h2>
<span className={`status ${order.status}`}>{order.status}</span>
</header>
<ul>
{order.items.map((item) => (
<li key={item.sku}>
{item.name} Γ {item.quantity} β ${item.price.toFixed(2)}
</li>
))}
</ul>
<footer>
<strong>Total: ${order.total.toFixed(2)}</strong>
</footer>
</article>
);
}
Why This Works
The OrderDetails component contains zero state declarations, zero effects, and zero conditional rendering logic. It assumes the data exists because React guarantees it. The use() hook suspends execution until orderPromise resolves, at which point React re-renders the component tree with the resolved value. If the Promise rejects, React bypasses the component entirely and renders the Error Boundary fallback. This eliminates the traditional loading/error/data branching pattern and replaces it with a declarative contract: the component receives data, or the boundary handles failure.
Pitfall Guide
1. Instantiating Promises Inside Render
Explanation: Creating a new Promise on every render causes use() to suspend repeatedly, triggering an infinite render loop. React treats each new Promise as a pending resource.
Fix: Lift Promise creation to a parent component, Server Component, or wrap it in useMemo/React Cache to maintain reference stability.
2. Assuming use() Caches Results
Explanation: use() does not cache resolved values. It only reads the Promise reference passed to it. If the parent re-renders and creates a new Promise, the consumer will suspend again.
Fix: Use React Cache (cache() from react/cache) or stable prop passing to ensure the same Promise instance is reused across renders.
3. Mixing use() with useEffect for the Same Data
Explanation: Combining use() with useEffect creates conflicting lifecycle models. useEffect runs after render, while use() pauses render. This leads to stale data or double-fetching.
Fix: Commit to one paradigm. If using use(), remove all useEffect data fetching logic for that resource.
4. Ignoring Error Boundaries
Explanation: use() delegates rejection handling to Error Boundaries. Without a boundary, unhandled Promise rejections crash the component tree or produce silent failures.
Fix: Always wrap use() consumers in <ErrorBoundary> or handle rejections at the route level. Never rely on try/catch inside the component body.
5. Using use() with Non-Promise/Non-Context Values
Explanation: use() strictly accepts Promises or Context objects. Passing primitives, objects, or React elements throws a runtime error.
Fix: Validate the argument type before calling use(). For local state, continue using useState. For context, pass the Context object directly.
6. Overlooking Client-Side Memoization
Explanation: In Client Components, dependencies that change (like route params) require new Promises. Failing to memoize these causes unnecessary suspension cycles.
Fix: Use useMemo with dependency arrays or leverage React Cache to stabilize Promise references across parameter changes.
7. Testing Without Mocking Suspense Boundaries
Explanation: Unit tests that render components using use() without wrapping them in <Suspense> will hang or throw. The test runner cannot resolve the suspended state.
Fix: Wrap test renders in <Suspense fallback={null}> and mock the Promise resolution using act() or testing library utilities.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Server-rendered pages with static routes | use() + Server Components | Zero client-side state, stable Promises, optimal TTFB | Low (infrastructure unchanged) |
| Client-side interactive dashboards | use() + useMemo/React Cache | Prevents infinite suspension, maintains interactivity | Medium (requires cache strategy) |
| Complex caching, deduplication, or background refetching | React Query / SWR | Built-in cache, stale-while-revalidate, devtools | High (additional dependency, learning curve) |
Legacy codebases with heavy useEffect usage | Gradual migration via boundary components | Minimizes refactoring risk, isolates use() adoption | Low (incremental rollout) |
Configuration Template
Use this template to standardize use() adoption across your codebase. It enforces boundary consistency and Promise stability.
// lib/data-boundary.tsx
import { Suspense, type ReactNode } from "react";
import { ErrorBoundary } from "react-error-boundary";
interface DataBoundaryProps {
children: ReactNode;
loadingFallback?: ReactNode;
errorFallback?: (error: Error) => ReactNode;
}
export function DataBoundary({
children,
loadingFallback = <div className="skeleton-loader" />,
errorFallback = (error) => <div className="error-state">{error.message}</div>,
}: DataBoundaryProps) {
return (
<ErrorBoundary fallbackRender={({ error }) => errorFallback(error)}>
<Suspense fallback={loadingFallback}>{children}</Suspense>
</ErrorBoundary>
);
}
// hooks/use-stable-promise.ts
import { useMemo } from "react";
export function useStablePromise<T>(factory: () => Promise<T>, deps: unknown[]): Promise<T> {
return useMemo(() => factory(), deps);
}
Quick Start Guide
- Create the Promise Externally: Move your
fetch() or API call to a parent component, Server Component, or custom hook. Ensure the Promise reference is stable.
- Pass the Promise as a Prop: Forward the Promise to the child component that needs the data. Do not unwrap it in the parent.
- Wrap in Boundaries: Surround the consumer with
<Suspense> and <ErrorBoundary> at the layout or route level.
- Unwrap with
use(): Inside the consumer component, call const data = use(promiseProp). Remove all useState, useEffect, and conditional rendering logic.
- Test the Flow: Verify that loading fallbacks appear immediately, errors bubble to the boundary, and resolved data renders without manual state transitions.