Topic 14.4
use(): Reading Promises and Context
In one line
use(resource) reads the value of a promise (suspending until it settles) or of a context; unlike other hooks it may be called inside if blocks and after early returns, but the promise must be created outside render or cached, never made fresh on every render.
Think of it like this
A ticket at a dry cleaner. When you drop off your shirt you get a ticket. Later you hand over the ticket and get the shirt, or hear "not ready yet, wait a moment". The ticket must be the same one you got at drop-off. If you print a new ticket every time you walk in, the cleaner starts a new wash each time and you never get your shirt. A promise is the ticket, use() is handing it over, and creating the promise during render is printing a new ticket on every visit.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- use
- A React 19 API that reads a promise or a context during render; it may be called in conditions.
- Pending / fulfilled / rejected
- The three states of a promise: still waiting, finished with a value, or failed with an error.
- Suspend
- What
usedoes with a pending promise: stop rendering this part and show the nearest Suspense fallback. - Stable promise
- The same promise object on every render, because it was created outside render or taken from a cache.
- Uncached promise
- A promise created fresh during render, which React can't track between retries.
- Render-as-you-fetch
- Start loading first (in an event or loader) and pass the promise down, so rendering and loading overlap.
Step by step
01Create the promise outside render
The parent starts the request once. A lazy useState initialiser runs only on the first render, so the promise object is created once and kept.
const [userPromise, setUserPromise] = useState(() => fetchUser(1));02Read it with use under a Suspense boundary
The child calls use(userPromise). While pending it suspends, so the boundary shows Loading user.... When it fulfils, React renders the child again and use returns the user.
function UserCard({ userPromise }) {
const user = use(userPromise);
return <p>{user.name}</p>;
}03Start the next request in the event
A click creates a new promise and stores it. Inside startTransition, React keeps the old user on screen and isPending is true until the new promise resolves.
<button onClick={() => startTransition(() => setUserPromise(fetchUser(2)))}>
Load user 2
</button>04Or cache promises by key
When the component itself knows the key (an id prop), get the promise from a cache. The same key returns the same object, so retries find a resolved promise. Real caches also need invalidation and size limits (Topic 14.7).
const cache = new Map();
export function getUser(id) {
if (!cache.has(id)) cache.set(id, fetchUser(id));
return cache.get(id); // same promise for the same id
}
function Profile({ id }) {
const user = use(getUser(id)); // safe: cached
return <h1>{user.name}</h1>;
}05Handle rejection with a boundary
Wrap the Suspense boundary in an error boundary. A rejected promise makes use throw, and the error boundary's fallback replaces the subtree. The second live example shows it.
06Read context conditionally
use(SomeContext) works like useContext, but you may call it after an early return or inside an if. The third live example reads the theme only for urgent notices.
Try it yourself
- 1
Remove the transition
In the first example, change the button to
onClick={() => setUserPromise(fetchUser(2))}. Predict the "after click" line. (The boundary's content was already visible, so without a transition React hides it and showsLoading user...again.) - 2
Add a cache and a user 3 button
Write
const cache = new Map(); const getUser = (id) => { if (!cache.has(id)) cache.set(id, fetchUser(id)); return cache.get(id); };and usegetUserin the buttons. Add buttons for users 1 and 2 and click them several times. How manyfetch userlines do you see? - 3
Recover with a default value
In the second example, make
Ordersreaduse(ordersPromise.catch(() => [])). Predict what happens. (Careful: that creates a new promise on every render. Moveconst safeOrders = ordersPromise.catch(() => []);to module level instead and read that.)
Code & diagrams
Each user is fetched exactly once. The first load shows the fallback; the second, inside a transition, keeps Asha visible with "loading..." until Ravi arrives.
Preview
Console
Nothing logged yet.Expected console output
fetch user 1
at 100 ms: Loading user...
at 700 ms: #1: Asha
fetch user 2
after click: loading... #1: Asha
later: #2: RaviPending: the Suspense fallback. Rejected: the error boundary's message. React also logs the caught error (the middle lines) in development.
Preview
Console
Nothing logged yet.Expected console output
screen: Loading orders...
Error: Server returned 500
The above error occurred in the <Orders> component.
React will try to recreate this component tree from scratch using the error boundary you provided, ErrorBoundary.
screen: Could not load: Server returned 500Changing the context re-renders every component that read it with `use`, the same as `useContext`. The hidden badge and the gray notice never read it.
Preview
Console
Nothing logged yet.Expected console output
New (light) | Notice in red | Notice in gray
New (dark) | Notice in orange | Notice in grayBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Create the promise during render
Write function Profile() { const user = use(fetchUser()); ... }, where fetchUser logs fetch #n and resolves after 200 ms.
Break #2
A fast uncached promise
Same component, but fetchUser returns Promise.resolve({ name: "Asha" }).
Myth vs fact
Myth
use is a hook, so it must follow every rule of hooks.
Fact
It must be called during render in a component or hook, but unlike other hooks it can be called in conditions, loops and after early returns. It can't be called in a try/catch.
Myth
use(fetch(url)) is the new way to fetch in a component.
Fact
That creates a new promise every render and never settles. use reads promises that were created elsewhere or cached.
Myth
use replaces data libraries.
Fact
use only reads a promise. Caching, deduplication, invalidation and refetching still come from a library, a framework or your own cache.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
React tracks promises passed to
useby settingstatusandvalue/reasonfields on them (the same convention libraries use). If you pass an already-fulfilled promise that hasstatus: "fulfilled"andvalueset,usereturns the value without suspending, which is how caches can avoid a fallback flash for data they already have. - ▸
When a component suspends inside a transition, React may replay the component as soon as the promise resolves (if it's quick) without throwing away the work; the uncached-promise warning comes from that replay comparing promise identities.
- ▸
With Server Components you can start a request on the server without awaiting it and pass the promise as a prop to a client component, which reads it with
use. The server streams the result when ready, and the client component suspends until then (Topic 20.2). - ▸
Libraries that support Suspense give you stable promises or suspend for you: React Router lets a loader return a promise without awaiting it and read it later with
<Await>oruse, and TanStack Query hasuseSuspenseQuery(Topic 14.6). Check each library's docs for its exact Suspense API.
Remember this
- 1
**What
useis.**useis an API added in React 19, imported fromreact:const value = use(resource). The resource is either a promise or a context object. For a context,use(ThemeContext)returns the current value exactly likeuseContext(Topic 9.2). For a promise, it returns the resolved value if the promise has fulfilled, throws the error if it has rejected, and suspends the component if it's still pending, so the nearest<Suspense>shows its fallback (Topic 14.3) until React retries. - 2
It may be called conditionally. All other hooks must run at the top level in the same order every render (Topic 4.3).
useis the exception: you may call it insideif, after an earlyreturn, or in a loop, because React doesn't store it in a numbered hook slot. It still must be called inside a component or a hook, during render (not in event handlers or effects), and not inside atry/catchblock: errors are handled by an error boundary or by attaching.catchto the promise before passing it. - 3
Promises must be stable. Each time React retries a suspended component, it calls the component function again. If that function creates the promise (
use(fetchUser(id))), every retry makes a new pending promise, which suspends again: the component never finishes and requests pile up. If the promise happens to resolve very quickly, React 19 also warns:A component was suspended by an uncached promise. Creating promises inside a Client Component or hook is not yet supported, except via a Suspense-compatible library or framework.So create the promise outside render: at module level, in an event handler, in a route loader, in a parent stored in state, or in a Server Component that passes it to a client component (Phase 20). Or cache it: aMapfrom key to promise so the same key returns the same promise object. - 4
**Render-as-you-fetch with
use.** The pattern is: start the request in an event or loader (setUserPromise(fetchUser(2))), pass the promise down as a prop, and let the child read it withuse. The request starts before the child renders, and the child's code reads like synchronous code: no loading flag, no effect. Wrap the update instartTransitionso the already-shown user stays visible while the next one loads, instead of the fallback flashing back. - 5
**Errors and
use.** If the promise rejects,usethrows the rejection reason during render, so the nearest error boundary (Topic 14.5) shows its fallback. To show a default value instead, catch it on the promise:const safe = promise.catch(() => ({ name: "Unknown" }))(createsafeoutside render too). Either way the screen gets a clear outcome for every state: Suspense handles pending, the error boundary handles rejected, and your component only handles fulfilled. - 6
When to use it.
use(promise)shines with frameworks and libraries that hand you cached promises (Server Components passing promises to client components, React Router's deferred loader data). In a plain client app you need your own cache to use it safely, which is why data libraries (Topic 14.6) remain the usual tool there.use(Context)is useful anywhere you need context conditionally.
Explain it without notes
What does use(promise) do for each state of the promise?
Why can use be called conditionally when useState and useContext cannot?
Why must a promise passed to use be cached or created outside render? What happens otherwise?
How do Suspense and error boundaries divide the work when you read data with use?
Practice
Write a getProduct(id) cache and a Product component that reads it with use, inside a Suspense boundary with a fallback.
Write a Greeting component that only reads UserContext (with use) when the prop personal is true, and says Hello! otherwise.
A parent should load comments when a button is clicked and pass them to Comments, which reads them with use. Write both, keeping the button page visible while loading.
Trade-offs
- ↔
usemakes components simple (no loading flags), but moves the complexity into where promises are created and cached. Without a framework or library, you write and maintain that cache yourself. - ↔
Conditional
use(Context)can save a context subscription in components that rarely need it, but mixinguseanduseContextin a codebase adds a choice for every reader; many teams standardise on one.
Done when you can
I can read a promise with
useinside a Suspense boundary.I never create the promise passed to
useduring render without a cache.I can start a request in an event handler and pass the promise down.
I can handle a rejected promise with an error boundary.
I can call
use(Context)conditionally and explain why that's allowed.