A6 · Part 3 · Advanced
The use() hook
Reading a promise or a context during render — and the only hook that breaks the rules.
10On this page
Reading a promise
function Comments({ commentsPromise }) {
const comments = use(commentsPromise); // suspends until it resolves
return <ul>{comments.map(...)}</ul>;
}
<Suspense fallback={<Skeleton />}>
<Comments commentsPromise={getComments()} />
</Suspense>
use unwraps a promise during render. If it is pending the
component suspends and the nearest
Suspense boundary shows its fallback; if it
rejects, the nearest error boundary catches it. No useEffect, no
loading flag, no error state in the component.
The pattern it enables
// server component — starts the request but does not await it
export default function Page() {
const commentsPromise = getComments(); // no await
return (
<>
<Article /> // renders immediately
<Suspense fallback={<Skeleton />}>
<Comments commentsPromise={commentsPromise} />
</Suspense>
</>
);
}
Awaiting in the server component would block the whole page on the slowest query. Passing the promise down lets the fast content render and stream immediately, while the slow part fills in later. This is the idiomatic way to avoid a server-side waterfall.
The value a server promise resolves to crosses the network, so it must be serialisable: plain objects, arrays, strings, numbers, dates — not class instances or functions.
Start requests together, read them apart
// ✗ a waterfall: the second request starts only after the first resolves
async function Page() {
const user = await getUser();
const posts = await getPosts();
}
// ✓ both start now; each part reveals when its own data lands
function Page() {
const userPromise = getUser();
const postsPromise = getPosts();
return (
<>
<Suspense fallback={<HeaderSkeleton />}><Header userPromise={userPromise} /></Suspense>
<Suspense fallback={<PostsSkeleton />}><Posts postsPromise={postsPromise} /></Suspense>
</>
);
}
Where a request starts and where it is read are separate
decisions, and use is what lets you separate them. Two
awaits in a row are sequential even when the requests are
independent. Creating both promises first makes them parallel, and the
boundaries decide what the user sees while they run. Two components can also
read the same promise; it is fetched once and both resume when it settles.
use or await?
| Where | Use | Why |
|---|---|---|
| Server Component, data this component renders | await | Server Components can be async; it is simpler and needs no boundary of its own |
| Server Component, data a child renders later | pass the promise | Starts the request early without blocking this component |
| Client Component | use(promise) | Client components cannot be async functions |
The rule of thumb: await where the data is needed to render the component you are in; hand over the promise where it is needed further down. Both can live in the same page.
The one hook that can be called conditionally
function Panel({ show, dataPromise }) {
if (!show) return null;
const data = use(dataPromise); // ✓ legal — after an early return
}
Every other hook depends on stable call order. use does not
store anything in the hook slot list, so it can sit inside a condition, a
loop, or after a return. It still cannot be called from a callback or an
effect — only while rendering a component or inside another hook.
It cannot go inside try/catch
// ✗ throws "Suspense Exception: This is not a real error!"
function Albums({ albumsPromise }) {
try {
const albums = use(albumsPromise);
} catch (e) {
return <p>Error</p>;
}
}
// ✓ the boundary is the catch
<ErrorBoundary fallback={<p>Error</p>}>
<Albums albumsPromise={albumsPromise} />
</ErrorBoundary>
Suspending works by interrupting the render, and a catch block
would swallow that interruption. When a rejection should become a value rather
than an error screen, handle it on the promise before passing it in:
const safe = getComments().catch(() => []); // rejection becomes an empty list
<Comments commentsPromise={safe} />
Reading context
function Item({ compact }) {
if (compact) {
const theme = use(ThemeContext); // ✓ conditional context read
...
}
}
use(SomeContext) does what useContext does, with the
same conditional freedom. Useful when a branch of a component needs context
that the other branch has no business subscribing to. Like
useContext, it looks for the closest provider above the
calling component and ignores one rendered by that component itself. Reading
context with use is not supported in Server Components.
Do not create the promise during render
function Comments({ id }) {
const data = use(fetch("/api/comments/" + id)); // ✗ new promise every render
}
Suspending re-runs the component when the promise resolves. If the promise is created inside the render, the retry creates another one, which suspends again — an infinite loop of requests. React warns with "A component was suspended by an uncached promise". The promise must come from somewhere stable: a prop from a Server Component, a cache, or a framework API.
Caching promises in client code
const cache = new Map();
export function fetchComments(id) {
if (!cache.has(id)) {
cache.set(id, fetch("/api/comments/" + id).then((r) => r.json()));
}
return cache.get(id); // same promise for the same id
}
function Comments({ id }) {
const comments = use(fetchComments(id)); // ✓ stable across retries
}
This is the smallest correct version, and seeing it explains why you usually
want a library instead. This cache never expires, never refetches, keeps a
rejected promise forever, and grows without limit. Query libraries and
frameworks solve exactly those problems, and then hand use a
stable promise.
On the server the equivalent is React's cache: wrap a data
function with it, and every Server Component that calls it with the same
arguments during one request shares a single call. It is scoped to that
request and only works in Server Components, so it deduplicates without ever
leaking one user's data into another's page.
Never peek at a promise's status or value to skip
calling use. Always pass the promise and let React decide;
bypassing it breaks Suspense optimisations and DevTools.
How it compares
use() | Effect + state | Query library | |
|---|---|---|---|
| Loading state | the boundary | you write it | returned to you |
| Errors | error boundary | you write it | returned to you |
| Caching | none of its own | none | yes |
| Refetch, invalidate | the framework's job | manual | yes |
| Where it fits | server-passed promises | legacy, or one-offs | client-side data |
use is not a replacement for a data library — it has no
cache and no revalidation. It is the primitive that makes promises readable
during render, which is what libraries and frameworks build on.
Where you will actually meet it
- A Server Component starting a slow query and passing the promise to a client component.
- Framework data APIs that hand you a promise instead of awaiting it for you.
- Conditional context reads, occasionally.
use(browser()), added in React 19.3, to render a part only on the client — covered in rendering strategies.
In a plain client-side app with a query library, you will rarely call it directly — and that is fine. Knowing what it does explains how Suspense gets its promises.
Interview answer, one sentence
"use reads a promise or a context during render, suspending
until it resolves, and it is the only hook that can be called conditionally
because it stores nothing in the hook list. Its main use is passing an
unawaited promise from a Server Component to a client one; the promise must
be cached, errors go to an error boundary rather than try/catch, and inside
a Server Component plain await is usually simpler."
Opens in the editor: write it, run it, and check it against real tests.
All 54 for this level →Finished reading?
0 of 57 chapters read in this series