React's use() Hook: Reading Promises and Context the New Way
React 19's use() reads promises and context right in render — even conditionally. How it works with Suspense and Server Components, plus the caching gotcha.
React 19 shipped an API with the shortest name in the framework and some of the most misunderstood semantics: use(). It reads a resource — a promise or a context — from inside render, and unlike every hook you know, it is allowed inside conditions and loops. That one exception is not an oversight; it is the whole design.
Used well, use() deletes a lot of useEffect-plus-useState data plumbing and makes Suspense feel like a language feature. Used carelessly, it produces a component that suspends forever while looking perfectly innocent. This guide covers both halves: the patterns worth adopting today, and the one gotcha that bites almost everyone once.
What use() actually is
use(resource) takes either a promise or a context object and returns its value. Two rules define it: it must be called during render (a component or a custom hook — not in event handlers, not in effects), and within render it may be called conditionally. That second rule is exactly what useContext and friends forbid, and it exists because use() does not occupy a slot in the hook list — React resolves it against the resource you pass, not against call order.
When the resource is a pending promise, the component suspends: React pauses that subtree, shows the nearest <Suspense> fallback, and replays the render when the promise settles. A rejected promise surfaces at the nearest error boundary. In other words, use() is the missing bridge between plain promises and the Suspense machinery React has had for years.
Reading context — finally, conditionally
The context half is the easy win. use(Context) behaves like useContext(Context) except you can call it after early returns and inside branches:
import { use } from "react";
function StatusDot({ live }) {
// Early return BEFORE reading context — illegal with useContext,
// perfectly fine with use().
if (!live) {
return null;
}
const theme = use(ThemeContext);
return <span className={theme.dotClass} />;
}No more hoisting a context read above a guard clause just to satisfy the rules of hooks, and no more reading context in components that skip it on 90% of renders. For unconditional reads, useContext still works and there is no urgency to migrate — new code simply has one less rule to remember.
Reading promises: the Server Component handshake
The promise half shines in one specific shape: a Server Component starts a fetch and passes the unawaited promise down; a Client Component unwraps it with use(). The server does not block on the slow data, the client streams it in, and Suspense handles the waiting state:
// page.jsx — Server Component (no "use client")
import { Suspense } from "react";
import { Comments } from "./comments";
export default function PostPage({ postId }) {
// Kick off the fetch, do NOT await it here.
const commentsPromise = fetchComments(postId);
return (
<article>
<PostBody postId={postId} />
<Suspense fallback={<CommentsSkeleton />}>
<Comments commentsPromise={commentsPromise} />
</Suspense>
</article>
);
}// comments.jsx — Client Component
"use client";
import { use } from "react";
export function Comments({ commentsPromise }) {
// Suspends until the promise resolves; Suspense shows the skeleton.
const comments = use(commentsPromise);
return (
<ul>
{comments.map((c) => (
<li key={c.id}>{c.text}</li>
))}
</ul>
);
}Compare that with the classic client-only version: a useEffect, two useStates, a loading flag, an ignore-stale-response guard. All of it disappears, and the page body renders immediately while comments stream in behind the skeleton.
If you work in Next.js, you have already met this pattern in the framework itself: since Next 15, params and searchParams are promises, and the documented way for a client page to read them is use(params). The framework is telling you what it expects idiomatic data flow to look like.
The gotcha: never create the promise in render
Here is the mistake everyone makes exactly once. Since use() accepts a promise, why not fetch right there?
// BROKEN — do not do this
function Profile({ userId }) {
const user = use(fetchUser(userId));
return <h1>{user.name}</h1>;
}Render calls fetchUser, which returns a new promise. The component suspends. When the promise resolves, React re-renders — and the re-render calls fetchUser again, producing another brand-new pending promise. Suspend, resolve, re-render, repeat: an infinite loading state (and in dev, React warns about an uncached promise). use() does not memoize anything for you — it needs to see the same promise across renders.
The fix is always some form of caching the promise outside render. The Server Component handshake above is one fix (the promise is created once, on the server). On the pure client, keep a cache keyed by your input:
// A tiny promise cache — module scope, survives re-renders
const userCache = new Map();
function getUser(userId) {
if (!userCache.has(userId)) {
userCache.set(userId, fetchUser(userId));
}
return userCache.get(userId);
}
function Profile({ userId }) {
const user = use(getUser(userId)); // same promise every render
return <h1>{user.name}</h1>;
}That Map is deliberately primitive — no invalidation, no deduping across users, no revalidation. The moment you find yourself extending it, you have rediscovered why data libraries exist.
Errors: rejected promises meet error boundaries
When a promise passed to use() rejects, the error propagates like a thrown render error: the nearest error boundary catches it. So the full production pattern is a Suspense boundary for the pending state and an error boundary for the failure state, wrapped around the same subtree. If you would rather render a fallback value than an error page, catch on the promise before it reaches use() — hand the component a promise that resolves to a default.
What use() does not replace
- React Query / SWR: caching, revalidation, mutations, optimistic updates —
use()is a reading primitive, not a data layer. The libraries are themselves adoptinguse()under the hood. useEffectfor genuine side effects: subscriptions, analytics, imperative APIs.use()reads values; it does not run effects.- Form state: submissions belong to actions and
useActionState— covered in our React 19 form actions guide. - Event-handler data fetching:
use()cannot be called there. Fetch in the handler, store the promise in state, read it withuse()on the next render.
A reasonable adoption rule for 2026: reach for use() when a Server Component can start the fetch and a Client Component needs the value, when you need a conditional context read, or when a framework hands you a promise. Keep your data library for everything with a cache lifetime. The two coexist happily — use() is the low-level verb the rest of the ecosystem is being rebuilt on.