use() is a small hook with a large blast radius. It lets components read promises as if they were synchronous values, with Suspense handling the wait. The win is that your component renders top-to-bottom in the success path; no loading state to thread through.
The shape
function MyComp({ promise }: { promise: Promise<T> }) {
const data = use(promise);
return <p>{data.value}</p>;
}
If the promise is pending, use() suspends — React shows the nearest Suspense fallback. If it rejects, use() throws — the nearest error boundary catches. If it's resolved, use() returns the value. The component body sees only the success case.
The cardinal rule — same promise across renders
use() recognizes a promise by reference identity. If you create a new promise inside the component on every render, use() sees a different (still pending) promise every time, suspends every time, and you get an infinite loading state.
The fix: create the promise outside the component (or in a parent and pass as prop, or cache it in a ref / context, or use a library like TanStack Query that handles caching).
Three sources of stable promises
Parent prop — parent creates the promise (once or via cached factory), child consumes via use().
Module-level constant — fine for one-off bootstrapping data.
Cached factory — a function like cache((id: string) => fetch(id).then(j)) from React's cache helper (or your own Map-backed memoization).
use() with Context
use() also reads Context — and unlike useContext, it can be called conditionally. if (someCondition) { const value = use(MyCtx); } is legal with use() but illegal with useContext. Convenient when you have a Context that some branches don't need.
use() shifts the responsibility for where async lives. Pre-use(), each component carried its own loading state. Post-use(), the wait state lives in the Suspense boundary higher up, and the component reads the value as if it were already there. The leaf gets simpler; the parent takes on the promise-creation responsibility.
Code
use() with a parent-created promise·tsx
import { use, Suspense } from "react";
async function fetchUser(id: string): Promise<{ name: string }> {
const r = await fetch(`/api/users/${id}`);
if (!r.ok) throw new Error(r.statusText);
return r.json();
}
function UserName({ userPromise }: { userPromise: Promise<{ name: string }> }) {
const user = use(userPromise);
return <p>{user.name}</p>;
}
function UserPage({ userId }: { userId: string }) {
// Parent creates the promise. Each render of UserPage with the same userId
// should yield the same promise — use a cache or pass through useState/useRef.
const promise = fetchUser(userId); // simplified — see the cache pattern below for real use
return (
<Suspense fallback={<p>Loading…</p>}>
<UserName userPromise={promise} />
</Suspense>
);
}
The cache pattern — stable promises across renders·tsx
import { use, Suspense } from "react";
// Map-backed cache so the same userId always returns the same promise.
const userCache = new Map<string, Promise<{ name: string }>>();
function getUserPromise(userId: string): Promise<{ name: string }> {
if (!userCache.has(userId)) {
userCache.set(
userId,
fetch(`/api/users/${userId}`).then((r) => r.json())
);
}
return userCache.get(userId)!;
}
function UserName({ userId }: { userId: string }) {
const user = use(getUserPromise(userId));
return <p>{user.name}</p>;
}
function UserPage({ userId }: { userId: string }) {
return (
<Suspense fallback={<p>Loading…</p>}>
<UserName userId={userId} />
</Suspense>
);
}
// In production you'd use TanStack Query or React's `cache()` helper
// to get the same effect with invalidation, retries, and deduping.
Build a <UserPage userId={...}> that uses the cache pattern shown. Confirm: (1) initial load suspends and shows the Suspense fallback; (2) once loaded, the data appears; (3) navigating to a different userId triggers another fetch + suspension; (4) navigating BACK to the original userId doesn't re-fetch (the cache hit returns the resolved promise). The cache makes navigation feel instant.
Hint
If repeat visits re-fetch, your cache is wrong. Check: is the Map module-level (preserved across renders) or local-to-component (recreated)? Module-level is what you want.
Progress
Progress is local-only — sign in to sync across devices.