The use() API: Reading Promises and Context
Understand what the newer use() API does, how it reads promises and context, its relationship to Suspense and error boundaries, and how it differs fundamentally from every other React hook.
use() (a newer, version-specific API introduced in React 19) lets a component read the resolved value of a promise or the current value of a Context directly during render. It looks like a hook and follows similar naming conventions, but it is deliberately not bound by the Rules of Hooks the way useState or useEffect are — most notably, it can be called conditionally, inside loops, and after early returns.
Reading a promise with use()
Passing a promise to use() causes the component to 'suspend' — pause rendering — until that promise settles. If it resolves, use() returns the resolved value directly, as a synchronous-looking value your component can use immediately in the same render pass. If it rejects, the error is thrown and caught by the nearest error boundary above the component in the tree, exactly like a promise-rejection error.
This is a different mental model from a typical data-fetching hook that returns something like { data, loading, error }, which requires the component to explicitly branch on loading and error before using data. With use(), the loading state is handled declaratively by wrapping the component in a Suspense boundary elsewhere in the tree, and the error state is handled declaratively by an error boundary — the component itself only has to handle the 'success' case, since it doesn't render at all during the pending or error paths.
A critical constraint: the promise passed to use() should come from a stable source — a cache, a framework's data-loading layer, or a promise created outside the component and passed down as a prop — rather than being created fresh inline during render (such as calling fetch(...) directly inside the component body every render). A freshly created promise on every render causes the component to suspend repeatedly instead of settling once, since React has no way to recognize it as 'the same' request across renders.
Reading context with use()
use() can also read a Context value, working similarly to useContext(SomeContext) in the common case — it returns the current value provided by the nearest matching <SomeContext.Provider> above the component in the tree, or the context's default value if there's no provider.
The key difference from useContext is where use() can be called from. useContext, like all standard hooks, must be called unconditionally at the top level of the component, before any early return and outside any if, loop, or nested function. use() has no such restriction — it can be called inside an if block, after an early return, or inside a loop, because of a fundamentally different internal design (explained next).
Why use() can be called conditionally when other hooks cannot
Ordinary hooks like useState rely on React tracking their calls by *order*: React keeps an ordered list of 'slots' per component instance and matches the Nth hook call on this render to the Nth hook call on the last render, by position. That's why those hooks must run in the exact same order every single render — skip one conditionally, and every hook call after it gets matched to the wrong slot.
use() is not implemented this way. It doesn't rely on call-order slot matching the way state-holding hooks do — it's designed to simply read whatever promise or context value is passed to it at the moment it's called, which is safe to do conditionally or in a loop since there's no persistent per-call state slot that ordering could desynchronize. This is what makes use() explicitly exempt from the 'top level only' half of the Rules of Hooks (it still must only be called during rendering, inside a component or another hook — that part of the rules still applies).
This is worth internalizing precisely: use() is exempt from the *ordering* requirement, not from being called during render. You still cannot call use() from a regular event handler or outside of component/hook render code — it must execute as part of a render pass, same as any other hook-like API.
Suspense, error handling, and framework/version requirements
Any component calling use() on a promise must have a Suspense boundary somewhere above it in the tree; without one, the suspension has nothing to catch it and React surfaces an error. The same Suspense boundary can wrap multiple components that each call use() on different promises — the fallback shows until all of them have resolved.
For error handling, pairing an error boundary with the Suspense boundary is standard practice: Suspense handles the pending state, and the error boundary catches a rejected promise (or a thrown error from use() reading context, though context reads themselves don't typically throw).
On the server/client boundary: use() works the same conceptually whether the promise originates from a Server Component being passed down to a Client Component, or from a purely client-side cache — but the exact rules for passing an unresolved promise across a server/client boundary depend on the framework (such as Next.js's App Router) built on top of React, so treat any specific cross-boundary promise-passing pattern as framework-documented behavior layered on top of the core use() API. use() itself requires React 19 or later; it does not exist in React 18 or earlier.
use() with a promise, inside Suspense
use() is a React 19+ API — it does not exist in React 18 or earlier. Comments must render inside a Suspense boundary since use() suspends the component while commentsPromise is pending. commentsPromise comes from a cache (getCachedComments), not a fresh fetch() call inline in the component, so it stays stable across re-renders.
import { use, Suspense } from 'react';
type Comment = { id: string; text: string };
function Comments({ commentsPromise }: { commentsPromise: Promise<Comment[]> }) {
const comments = use(commentsPromise);
return (
<ul>
{comments.map((c) => (
<li key={c.id}>{c.text}</li>
))}
</ul>
);
}
function Article({ articleId }: { articleId: string }) {
// Created once per articleId change via a cache, not fresh every render
const commentsPromise = getCachedComments(articleId);
return (
<Suspense fallback={<p>Loading comments…</p>}>
<Comments commentsPromise={commentsPromise} />
</Suspense>
);
}Code Examples
use(ThemeContext) is only reached on renders where showThemedBorder is true and the function has already passed the early return — exactly the pattern that is forbidden for useContext, but explicitly allowed for use() because it isn't governed by call-order slot matching.
const theme = use(ThemeContext);
Common mistake: Assuming any other hook, like useContext or useState, could be substituted here safely — doing so would violate the Rules of Hooks and produce inconsistent behavior across renders.
In the BAD version, every re-render (even one unrelated to userId) calls fetch() again inline, creating a brand-new promise that use() has never seen before, so the component suspends again instead of just returning the already-known value. Expected result: The BAD version shows the Suspense fallback repeatedly on unrelated re-renders; the GOOD version only suspends once per distinct userId, thanks to the cache.
import { use, createContext } from 'react';
const ThemeContext = createContext<'light' | 'dark'>('light');
function Banner({ showThemedBorder }: { showThemedBorder: boolean }) {
if (!showThemedBorder) {
return <div className="banner">Welcome</div>;
}
// Only called on some renders, after an early return —
// this would be a Rules of Hooks violation with useContext.
const theme = use(ThemeContext);
return <div className={`banner banner--${theme}`}>Welcome</div>;
}// BAD: fetch() is called fresh on every render, producing a new
// promise each time, which makes the component suspend repeatedly.
function UserProfile({ userId }: { userId: string }) {
const user = use(fetch(`/api/users/${userId}`).then((r) => r.json()));
return <p>{user.name}</p>;
}
// GOOD: the promise is created once per userId and cached/memoized
// outside of this component's render, e.g. via a cache keyed by userId.
function UserProfileFixed({ userId }: { userId: string }) {
const user = use(getCachedUser(userId));
return <p>{user.name}</p>;
}use() vs. ordinary hooks: call-order matching
use() reads its argument directly rather than relying on a persistent ordered slot, which is why it tolerates conditional calls.
Ordinary hooks (useState, useEffect, ...):
Render 1: useState() -> slot0 useEffect() -> slot1
Render 2: useState() -> slot0 useEffect() -> slot1 (must match by position, every render)
use():
Render 1: (early return) -- use() not called
Render 2: if (cond) { use(promiseOrContext) } -- fine, no slot to desync
No per-call state slot is tracked, so conditional calls don't break anything.Important Rules
use() is a newer, version-specific API requiring React 19 or later — it does not exist in React 18 or earlier.
Unlike every other hook, use() may be called conditionally, inside loops, or after an early return — but it must still be called during a component's (or hook's) render, never from an event handler or outside render entirely.
A component calling use() on a promise needs a Suspense boundary somewhere above it in the tree; a rejected promise needs an error boundary to catch it.
Pass use() a stable promise (from a cache or a prop), not a promise freshly created inline on every render, or the component will suspend repeatedly.
Where this shows up
use() is most relevant in frameworks and data-loading patterns that hand a component a promise from a route loader, a server component, or a client-side cache — letting the component read that data directly without managing its own loading/error state variables. It's also useful for reading Context conditionally in components that need to branch on some condition before deciding whether a themed or feature-flagged value is even relevant.
function Comments({ postId }: { postId: string }) { const comments = use(fetch(/api/posts/${postId}/comments).then((r) => r.json())); return <ul>{comments.map((c: any) => <li key={c.id}>{c.text}</li>)}</ul>; } function Comments({ commentsPromise }: { commentsPromise: Promise<Comment[]> }) { const comments = use(commentsPromise); // promise created/cached by the caller return <ul>{comments.map((c) => <li key={c.id}>{c.text}</li>)}</ul>; }
Creating the promise inline inside the component (via fetch(...).then(...)) means a new promise instance exists on every render, so use() has no way to recognize it as already-resolved — the fix passes in an already-created, cacheable promise instead.
[true-false] use() can be called conditionally inside an if statement, unlike useState. A. True B. False
A. True — use() is explicitly exempt from the Rules of Hooks ordering requirement that applies to useState, useEffect, and other standard hooks, because it doesn't rely on call-order slot matching.
[multiple-choice] What must wrap a component that calls use() on a promise? A. A try/catch block B. A Suspense boundary somewhere above it in the tree C. A memo() wrapper D. Nothing extra is required
B. A Suspense boundary somewhere above it in the tree — use() suspends the component while the promise is pending; without a Suspense boundary above it to show a fallback, React has nothing to catch that suspension with.
[multiple-choice] Why should the promise passed to use() come from a cache rather than being created fresh inline every render? A. use() only accepts promises created with a special factory function B. A freshly created promise each render is never recognized as already-resolved, causing repeated suspension C. Inline promises cause a TypeScript compile error D. It has no effect either way
B. A freshly created promise each render is never recognized as already-resolved, causing repeated suspension — React tracks whether a specific promise instance has resolved; a new promise created on every render is a new instance each time as far as use() is concerned, so the component keeps suspending instead of settling once.
Explain, in your own words, why use() can safely be called after an early return in a component, while useState cannot. Then describe one situation where you would still need a Suspense boundary even if you're only using use() to read Context, not a promise.
useState (and most hooks) rely on React matching each call to a persistent state slot by the position/order it was called in across renders — skip a call conditionally and every later hook call shifts to the wrong slot. use() doesn't rely on any such ordered slot; it just reads whatever promise or context value it's given at the moment of the call, so calling it conditionally doesn't desynchronize anything. You'd still need a Suspense boundary in a component that calls use() on Context AND on a promise elsewhere in its body (or in a child it renders) — the Context read itself doesn't need Suspense, but the promise read in the same render does.
Think about slot-based matching vs. direct reads. For the second part, consider that a component might use() both a promise and a context.