React 19 use() Hook: Guide to Promises and Context
Master React 19's use() API for reading promises and context in render. Learn Suspense integration, error boundaries, and data fetching patterns.
Published:
Last Updated:
Introduction
React 19, stable since December 2024, introduced one of the most significant API additions in the library’s history: use(). After more than a year in production across thousands of applications, use() has fundamentally changed how React developers think about data fetching, asynchronous operations, and context consumption.
For years, the standard pattern for data fetching in React looked something like this: mount the component, fire off a useEffect, manage loading and error states with useState, and deal with cleanup and race conditions manually. Every component that needed server data repeated this same boilerplate. The result was verbose code, request waterfalls where child components had to wait for parents to render before initiating their own fetches, and an inconsistent user experience as different developers handled loading and error states differently.
The use() API solves these problems by letting you read the value of a resource — specifically a Promise or a Context — directly during render. When you pass a Promise to use(), React suspends the component until that Promise resolves, delegating loading states to the nearest Suspense boundary and errors to the nearest Error Boundary. The result is dramatically cleaner component code that separates the concern of “what data do I need” from “how do I show loading and error states.”
One important clarification before we go further: use() is technically not a hook. The React team refers to it as an API. While it looks like a hook and is imported from react alongside hooks, it does not follow the rules of hooks. You can call use() inside conditionals, loops, and after early returns. This is a deliberate design choice that makes it uniquely flexible compared to useState, useEffect, and every other hook in React’s API.
This guide covers everything you need to know about use() in 2026: how it works under the hood, practical patterns for both Promise and Context consumption, migration strategies from useEffect-based data fetching, advanced patterns with Server Components, and the common mistakes that trip up teams adopting it for the first time.
How use() Works
Syntax
The API surface of use() is deceptively simple:
import { use } from "react";
const value = use(resource);
The resource parameter accepts exactly two types:
- A Promise —
use()suspends the component until the Promise resolves, then returns the resolved value. - A Context —
use()reads the current value of a React Context, similar touseContext().
That is the entire API. There are no options, no configuration objects, no generics to wrangle. The power comes from how use() integrates with React’s existing Suspense and Error Boundary mechanisms.
Integration with Suspense
When you pass a Promise to use(), React does not wait inline for the Promise to resolve. Instead, it “suspends” the component. Suspension means React throws a special internal exception (you never see this yourself) that signals to the nearest <Suspense> boundary that this component is not ready to render yet.
The Suspense boundary catches this signal and renders its fallback prop instead. When the Promise resolves, React re-renders the suspended component with the resolved value, replacing the fallback with the actual content.
import { Suspense, use } from "react";
function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
const user = use(userPromise);
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
function App() {
const userPromise = fetchUser(1);
return (
<Suspense fallback={<div>Loading profile...</div>}>
<UserProfile userPromise={userPromise} />
</Suspense>
);
}
This pattern inverts the traditional data fetching model. Instead of the component owning the fetch lifecycle (triggering it, tracking its state, cleaning it up), the component simply declares “I need this data” and React handles the rest.
Integration with Error Boundaries
If the Promise passed to use() rejects, React propagates the error to the nearest Error Boundary. This is the same mechanism React uses for render-time errors, so if you already have Error Boundaries in your application, they will catch rejected promises from use() automatically.
import { Suspense, use } from "react";
function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
const user = use(userPromise); // if this rejects, Error Boundary catches it
return <h1>{user.name}</h1>;
}
class ErrorBoundary extends React.Component<
{ fallback: React.ReactNode; children: React.ReactNode },
{ hasError: boolean; error: Error | null }
> {
state = { hasError: false, error: null };
static getDerivedStateFromError(error: Error) {
return { hasError: true, error };
}
render() {
if (this.state.hasError) {
return this.props.fallback;
}
return this.props.children;
}
}
function App() {
const userPromise = fetchUser(1);
return (
<ErrorBoundary fallback={<div>Something went wrong.</div>}>
<Suspense fallback={<div>Loading...</div>}>
<UserProfile userPromise={userPromise} />
</Suspense>
</ErrorBoundary>
);
}
Component Suspension Lifecycle
Here is exactly what happens when a component calls use() with a pending Promise:
- React begins rendering the component.
- The component calls
use(promise). - React checks whether the Promise has already resolved. If it has, React returns the value immediately and rendering continues.
- If the Promise is still pending, React throws a suspension signal.
- React walks up the component tree until it finds the nearest
<Suspense>boundary. - The Suspense boundary renders its
fallbackUI. - When the Promise resolves, React triggers a re-render of the suspended subtree.
- The component calls
use(promise)again. This time the Promise is resolved, souse()returns the value synchronously. - The component renders with the resolved data, replacing the fallback.
If the Promise rejects at step 7, React instead propagates the error upward to the nearest Error Boundary.
use() with Promises
Basic Example: Fetching User Data
The most common use case for use() is reading the result of an asynchronous data fetch. Here is a complete example with a typed API response:
import { Suspense, use } from "react";
interface User {
id: number;
name: string;
email: string;
role: string;
}
// This function returns a Promise - it does NOT await
function fetchUser(id: number): Promise<User> {
return fetch(`/api/users/${id}`).then((res) => {
if (!res.ok) throw new Error(`Failed to fetch user ${id}`);
return res.json();
});
}
function UserCard({ userPromise }: { userPromise: Promise<User> }) {
const user = use(userPromise);
return (
<div className="user-card">
<h2>{user.name}</h2>
<p>{user.email}</p>
<span className="badge">{user.role}</span>
</div>
);
}
export default function UsersPage() {
// Create the promise outside the suspended component
const userPromise = fetchUser(42);
return (
<Suspense fallback={<UserCardSkeleton />}>
<UserCard userPromise={userPromise} />
</Suspense>
);
}
Notice how UserCard contains zero state management. No useState for loading, no useState for error, no useEffect for triggering the fetch. The component is a pure function of its data.
Nested Suspense Boundaries
You can nest <Suspense> boundaries to create granular loading states. Each suspended component will show the fallback of its nearest ancestor boundary:
import { Suspense, use } from "react";
function UserHeader({ userPromise }: { userPromise: Promise<User> }) {
const user = use(userPromise);
return <h1>Welcome, {user.name}</h1>;
}
function UserPosts({ postsPromise }: { postsPromise: Promise<Post[]> }) {
const posts = use(postsPromise);
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
function UserStats({ statsPromise }: { statsPromise: Promise<Stats> }) {
const stats = use(statsPromise);
return (
<div>
<span>{stats.followers} followers</span>
<span>{stats.posts} posts</span>
</div>
);
}
export default function ProfilePage({ userId }: { userId: number }) {
// All three fetches start simultaneously
const userPromise = fetchUser(userId);
const postsPromise = fetchUserPosts(userId);
const statsPromise = fetchUserStats(userId);
return (
<div>
<Suspense fallback={<HeaderSkeleton />}>
<UserHeader userPromise={userPromise} />
</Suspense>
<div className="grid">
<Suspense fallback={<PostsSkeleton />}>
<UserPosts postsPromise={postsPromise} />
</Suspense>
<Suspense fallback={<StatsSkeleton />}>
<UserStats statsPromise={statsPromise} />
</Suspense>
</div>
</div>
);
}
In this example, the header, posts, and stats sections each have their own Suspense boundary. Each section appears independently as its data arrives. The user sees the profile header the moment the user data loads, without waiting for posts or stats to finish. This eliminates the waterfall problem that plagues useEffect-based approaches.
Error Handling with Error Boundaries
You can place Error Boundaries at different levels of granularity. A fine-grained approach lets one section fail without breaking the entire page:
import { Suspense, use, Component, type ReactNode } from "react";
class SectionErrorBoundary extends Component<
{ children: ReactNode; section: string },
{ hasError: boolean }
> {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
render() {
if (this.state.hasError) {
return (
<div className="error-card">
<p>Failed to load {this.props.section}.</p>
<button onClick={() => this.setState({ hasError: false })}>
Retry
</button>
</div>
);
}
return this.props.children;
}
}
export default function Dashboard() {
const revenuePromise = fetchRevenue();
const ordersPromise = fetchRecentOrders();
const analyticsPromise = fetchAnalytics();
return (
<div className="dashboard">
<SectionErrorBoundary section="revenue">
<Suspense fallback={<RevenueSkeleton />}>
<RevenueChart revenuePromise={revenuePromise} />
</Suspense>
</SectionErrorBoundary>
<SectionErrorBoundary section="orders">
<Suspense fallback={<OrdersSkeleton />}>
<RecentOrders ordersPromise={ordersPromise} />
</Suspense>
</SectionErrorBoundary>
<SectionErrorBoundary section="analytics">
<Suspense fallback={<AnalyticsSkeleton />}>
<AnalyticsPanel analyticsPromise={analyticsPromise} />
</Suspense>
</SectionErrorBoundary>
</div>
);
}
If the analytics API fails, the revenue chart and recent orders still render normally. The analytics section shows its own error message with a retry button.
use() with Context
Reading Context with use() vs useContext
use() can read React Context, serving as an alternative to useContext():
import { use, createContext } from "react";
const ThemeContext = createContext<"light" | "dark">("light");
// With useContext (traditional)
function ThemedButtonOld() {
const theme = useContext(ThemeContext);
return <button className={theme}>Click me</button>;
}
// With use() (new)
function ThemedButtonNew() {
const theme = use(ThemeContext);
return <button className={theme}>Click me</button>;
}
For this simple case, the two approaches are functionally identical. The distinction matters when you need conditional context reading.
The Key Difference: Conditional Context Reading
The useContext hook must be called at the top level of a component. You cannot put it inside an if statement, a loop, or after an early return. This is a fundamental rule of hooks that React enforces.
use() does not have this restriction. Because it is an API, not a hook, you can call it conditionally:
import { use, createContext } from "react";
const ThemeContext = createContext<"light" | "dark">("light");
const AdminContext = createContext<AdminConfig | null>(null);
function FeaturePanel({ isAdmin }: { isAdmin: boolean }) {
const theme = use(ThemeContext);
// This would be ILLEGAL with useContext
// But it is perfectly valid with use()
if (isAdmin) {
const adminConfig = use(AdminContext);
return (
<div className={`panel ${theme}`}>
<h2>Admin Panel</h2>
<p>Secret key: {adminConfig?.secretDashboardUrl}</p>
</div>
);
}
return (
<div className={`panel ${theme}`}>
<h2>User Panel</h2>
<p>Standard features only.</p>
</div>
);
}
Example: Theme Context for Specific Components
Consider a design system where some components opt into theming and others render with a fixed style:
import { use, createContext } from "react";
interface ThemeConfig {
primary: string;
secondary: string;
fontFamily: string;
}
const ThemeContext = createContext<ThemeConfig>({
primary: "#007bff",
secondary: "#6c757d",
fontFamily: "Inter, sans-serif",
});
function Card({
title,
children,
themed = true,
}: {
title: string;
children: React.ReactNode;
themed?: boolean;
}) {
// Only consume theme context when the card should be themed
if (themed) {
const theme = use(ThemeContext);
return (
<div
style={{
borderColor: theme.primary,
fontFamily: theme.fontFamily,
}}
>
<h3 style={{ color: theme.primary }}>{title}</h3>
{children}
</div>
);
}
// Non-themed cards use system defaults
return (
<div className="card-default">
<h3>{title}</h3>
{children}
</div>
);
}
Example: Auth Context After Route Check
Another practical pattern is reading auth context only when the route requires authentication:
import { use, createContext } from "react";
interface AuthUser {
id: string;
name: string;
permissions: string[];
}
const AuthContext = createContext<AuthUser | null>(null);
const PUBLIC_ROUTES = ["/", "/about", "/pricing", "/login"];
function PageContent({ route, contentPromise }: {
route: string;
contentPromise: Promise<PageData>;
}) {
const content = use(contentPromise);
if (!PUBLIC_ROUTES.includes(route)) {
const user = use(AuthContext);
if (!user) {
return <RedirectToLogin />;
}
if (content.requiredPermission && !user.permissions.includes(content.requiredPermission)) {
return <Forbidden />;
}
}
return <PageRenderer content={content} />;
}
The AuthContext is only read when the route is not public. This avoids unnecessary context subscriptions for public pages and keeps the auth check logic co-located with the rendering logic.
The Rules of use()
use() is more permissive than traditional hooks, but it still has rules. Understanding what you can and cannot do with use() is critical to avoiding subtle bugs.
What You CAN Do
Call use() inside conditionals:
function Component({ showDetails }: { showDetails: boolean }) {
if (showDetails) {
const details = use(detailsPromise);
return <Details data={details} />;
}
return <Summary />;
}
Call use() inside loops:
function MultiDataComponent({ promises }: { promises: Promise<Data>[] }) {
const results = [];
for (const promise of promises) {
results.push(use(promise));
}
return <DataGrid items={results} />;
}
Call use() after early returns:
function MaybeData({ shouldFetch, dataPromise }: {
shouldFetch: boolean;
dataPromise: Promise<Data>;
}) {
if (!shouldFetch) {
return <Placeholder />;
}
const data = use(dataPromise);
return <DataView data={data} />;
}
What You CANNOT Do
Do NOT call use() in a try-catch block:
// WRONG - this will not work as expected
function BadComponent({ dataPromise }: { dataPromise: Promise<Data> }) {
try {
const data = use(dataPromise);
return <DataView data={data} />;
} catch (error) {
// This catches React's internal suspension signal, not the actual error
return <ErrorView />;
}
}
use() works by throwing a special object when a Promise is pending. A try-catch block intercepts this internal mechanism and breaks Suspense. Always use Error Boundaries for error handling instead.
Do NOT create Promises during render:
// WRONG - creates a new Promise on every render, causing infinite suspension
function BadComponent({ userId }: { userId: number }) {
const data = use(fetch(`/api/users/${userId}`).then((r) => r.json()));
return <UserView data={data} />;
}
Each render creates a new Promise object, which React treats as a new resource, causing suspension again and again in an infinite loop. Promises must be created outside the render cycle.
Do NOT call use() outside a Component or Hook:
// WRONG - use() must be called within a React component or custom hook
const data = use(somePromise); // Error at module level
function helper() {
return use(somePromise); // Error in a regular function
}
Rules Comparison Table
Here is a side-by-side comparison of the rules for use() versus traditional hooks:
| Rule | use() | Traditional Hooks (useState, useEffect, etc.) |
|---|---|---|
| Call at top level | Optional | Required |
| Call inside conditionals | Yes | No |
| Call inside loops | Yes | No |
| Call after early returns | Yes | No |
| Call in try-catch | No | Yes (but should not for async) |
| Call in components | Yes | Yes |
| Call in custom hooks | Yes | Yes |
| Call in regular functions | No | No |
| Call in class components | No | No |
Data Fetching Patterns
Pattern 1: Server Component Passes Promise to Client Component
This is the recommended pattern in frameworks like Next.js. The Server Component initiates the data fetch, and the Client Component reads the result with use():
// app/users/[id]/page.tsx (Server Component)
import { Suspense } from "react";
import UserProfile from "./UserProfile";
async function fetchUser(id: string): Promise<User> {
const res = await fetch(`https://api.example.com/users/${id}`, {
next: { revalidate: 60 },
});
if (!res.ok) throw new Error("Failed to fetch user");
return res.json();
}
export default function UserPage({ params }: { params: { id: string } }) {
// Start the fetch but do NOT await - pass the Promise directly
const userPromise = fetchUser(params.id);
return (
<Suspense fallback={<ProfileSkeleton />}>
<UserProfile userPromise={userPromise} />
</Suspense>
);
}
// app/users/[id]/UserProfile.tsx (Client Component)
"use client";
import { use } from "react";
interface User {
id: string;
name: string;
email: string;
bio: string;
avatarUrl: string;
}
export default function UserProfile({
userPromise,
}: {
userPromise: Promise<User>;
}) {
const user = use(userPromise);
return (
<div className="profile">
<img src={user.avatarUrl} alt={user.name} />
<h1>{user.name}</h1>
<p>{user.bio}</p>
<a href={`mailto:${user.email}`}>{user.email}</a>
</div>
);
}
This pattern is powerful because the Server Component starts the fetch before any client-side JavaScript executes. The data may already be available by the time the Client Component hydrates on the browser.
Pattern 2: Route-Level Data Loading
Frameworks like React Router and TanStack Router support loader functions that run before the route renders. These pair naturally with use():
// Using React Router's loader pattern
import { use } from "react";
import { useLoaderData } from "react-router";
// The loader runs before the component renders
export async function loader({ params }: { params: { id: string } }) {
return {
userPromise: fetchUser(params.id),
postsPromise: fetchUserPosts(params.id),
};
}
export default function UserRoute() {
const { userPromise, postsPromise } = useLoaderData<typeof loader>();
return (
<div>
<Suspense fallback={<UserSkeleton />}>
<UserHeader userPromise={userPromise} />
</Suspense>
<Suspense fallback={<PostsSkeleton />}>
<UserPosts postsPromise={postsPromise} />
</Suspense>
</div>
);
}
Pattern 3: Cache Layer with use()
To avoid recreating promises on every render, use a cache layer. React provides a cache() function for Server Components, and you can build your own for Client Components:
// Simple client-side cache for use()
const promiseCache = new Map<string, Promise<unknown>>();
function cachedFetch<T>(key: string, fetcher: () => Promise<T>): Promise<T> {
if (!promiseCache.has(key)) {
promiseCache.set(key, fetcher());
}
return promiseCache.get(key) as Promise<T>;
}
// Usage in a component
function UserList() {
const usersPromise = cachedFetch("users", () =>
fetch("/api/users").then((r) => r.json())
);
return (
<Suspense fallback={<ListSkeleton />}>
<UserListInner usersPromise={usersPromise} />
</Suspense>
);
}
function UserListInner({ usersPromise }: { usersPromise: Promise<User[]> }) {
const users = use(usersPromise);
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
Anti-Pattern: Creating Promises in Render
This is the single most common mistake developers make with use(), and it deserves emphasis:
// ANTI-PATTERN: This creates an infinite loop
function BrokenComponent({ userId }: { userId: number }) {
// Every render creates a new Promise
// React sees a new Promise -> suspends -> re-renders -> new Promise -> suspends...
const user = use(
fetch(`/api/users/${userId}`).then((r) => r.json())
);
return <div>{user.name}</div>;
}
The fix is always to create the Promise outside the component that calls use() on it:
// CORRECT: Promise is created in the parent and passed as a prop
function FixedParent({ userId }: { userId: number }) {
// useMemo keeps the Promise stable across re-renders
const userPromise = useMemo(() => fetchUser(userId), [userId]);
return (
<Suspense fallback={<Loading />}>
<UserDisplay userPromise={userPromise} />
</Suspense>
);
}
function UserDisplay({ userPromise }: { userPromise: Promise<User> }) {
const user = use(userPromise);
return <div>{user.name}</div>;
}
When to Use use() vs useEffect vs TanStack Query
The right tool depends on your situation:
Use use() when:
- You are working within a framework that provides data loaders (Next.js, Remix, React Router)
- A Server Component can initiate the fetch and pass the Promise down
- You want Suspense-based loading states
- The data fetch is a one-time read, not a subscription
Use useEffect when:
- You need to set up a subscription (WebSocket, event listener, observable)
- You need to perform side effects that are not data fetching (DOM manipulation, analytics)
- You are working outside a framework and do not have a caching layer
- You need to imperatively control when a fetch happens (e.g., fetch on button click, not on render)
Use TanStack Query (or SWR) when:
- You need automatic refetching, polling, or stale-while-revalidate
- You need mutations with optimistic updates
- You need shared cache across components
- You need pagination, infinite scroll, or complex cache invalidation
- You want a mature, battle-tested data fetching layer with devtools
Note that TanStack Query v5+ integrates with Suspense and can be used alongside use(). They are complementary, not competing, tools.
Migration from useEffect
Before: The useEffect Pattern
Here is a typical data-fetching component written with the pre-React 19 useEffect pattern:
import { useState, useEffect } from "react";
interface Article {
id: number;
title: string;
body: string;
author: string;
publishedAt: string;
}
function ArticlePage({ articleId }: { articleId: number }) {
const [article, setArticle] = useState<Article | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
let cancelled = false;
setIsLoading(true);
setError(null);
fetch(`/api/articles/${articleId}`)
.then((res) => {
if (!res.ok) throw new Error("Failed to fetch article");
return res.json();
})
.then((data) => {
if (!cancelled) {
setArticle(data);
setIsLoading(false);
}
})
.catch((err) => {
if (!cancelled) {
setError(err);
setIsLoading(false);
}
});
return () => {
cancelled = true;
};
}, [articleId]);
if (isLoading) return <ArticleSkeleton />;
if (error) return <ErrorMessage error={error} />;
if (!article) return null;
return (
<article>
<h1>{article.title}</h1>
<p className="meta">
By {article.author} on {article.publishedAt}
</p>
<div>{article.body}</div>
</article>
);
}
That is 45 lines of code for a single data fetch. There are three pieces of state, a cleanup function for race conditions, and three conditional returns for different UI states.
After: The use() Pattern
Here is the same functionality with use():
import { Suspense, use } from "react";
interface Article {
id: number;
title: string;
body: string;
author: string;
publishedAt: string;
}
function fetchArticle(id: number): Promise<Article> {
return fetch(`/api/articles/${id}`).then((res) => {
if (!res.ok) throw new Error("Failed to fetch article");
return res.json();
});
}
function ArticleContent({ articlePromise }: { articlePromise: Promise<Article> }) {
const article = use(articlePromise);
return (
<article>
<h1>{article.title}</h1>
<p className="meta">
By {article.author} on {article.publishedAt}
</p>
<div>{article.body}</div>
</article>
);
}
function ArticlePage({ articleId }: { articleId: number }) {
const articlePromise = fetchArticle(articleId);
return (
<ErrorBoundary fallback={<ErrorMessage />}>
<Suspense fallback={<ArticleSkeleton />}>
<ArticleContent articlePromise={articlePromise} />
</Suspense>
</ErrorBoundary>
);
}
The component body went from 45 lines to about 15. There is no state management, no cleanup function, and no conditional returns for loading and error states. Those responsibilities are handled declaratively by Suspense and Error Boundary.
Step-by-Step Migration Guide
Here is how to migrate a useEffect data fetching component to use():
Step 1: Extract the fetch into a standalone function that returns a Promise.
Move your fetch call out of the useEffect callback and into a regular function. Do not await — just return the Promise.
// Before: fetch inside useEffect
useEffect(() => {
fetch(`/api/users/${id}`).then(res => res.json()).then(setUser);
}, [id]);
// After: standalone function
function fetchUser(id: number): Promise<User> {
return fetch(`/api/users/${id}`).then((res) => {
if (!res.ok) throw new Error("Failed to fetch");
return res.json();
});
}
Step 2: Split the component into a wrapper and a data consumer.
The wrapper creates the Promise and provides Suspense. The consumer calls use().
// Wrapper (creates the Promise, provides Suspense)
function UserPageWrapper({ userId }: { userId: number }) {
const userPromise = fetchUser(userId);
return (
<Suspense fallback={<Loading />}>
<UserPage userPromise={userPromise} />
</Suspense>
);
}
// Consumer (reads the Promise with use())
function UserPage({ userPromise }: { userPromise: Promise<User> }) {
const user = use(userPromise);
return <div>{user.name}</div>;
}
Step 3: Remove the useState and useEffect calls.
Delete all the loading, error, and data state. Delete the useEffect entirely. Delete the conditional rendering logic for loading and error states.
Step 4: Add Error Boundaries.
Wrap your Suspense boundary with an Error Boundary to handle fetch failures.
Step 5: Ensure Promise stability.
If the wrapper component re-renders, make sure you are not creating a new Promise each time. Use useMemo if the Promise depends on props, or create it in a parent Server Component or route loader.
What NOT to Migrate
Not everything that uses useEffect should be migrated to use(). The following patterns should stay with useEffect:
- Subscriptions: WebSocket connections, event listeners, observables, and any long-lived connection that pushes data over time.
- DOM manipulation: Measuring elements, setting up intersection observers, managing focus.
- Timers and intervals: Anything using
setTimeoutorsetInterval. - Analytics and logging: Tracking page views, firing events on mount.
- Imperative API calls: Fetches triggered by user actions (button clicks, form submissions) are better handled with actions and
useActionState.
The rule of thumb: if the operation is a one-time data read that happens at render time, consider use(). If it is a side effect, a subscription, or triggered by user interaction, keep useEffect or use actions.
Advanced Patterns
Parallel Data Fetching with Promise.all
You can combine multiple promises to fetch data in parallel and read the results with a single use() call:
import { Suspense, use, useMemo } from "react";
interface DashboardData {
user: User;
notifications: Notification[];
stats: DashboardStats;
}
function fetchDashboardData(userId: string): Promise<DashboardData> {
return Promise.all([
fetchUser(userId),
fetchNotifications(userId),
fetchDashboardStats(userId),
]).then(([user, notifications, stats]) => ({
user,
notifications,
stats,
}));
}
function DashboardContent({
dataPromise,
}: {
dataPromise: Promise<DashboardData>;
}) {
const { user, notifications, stats } = use(dataPromise);
return (
<div className="dashboard">
<WelcomeBanner user={user} />
<NotificationList notifications={notifications} />
<StatsGrid stats={stats} />
</div>
);
}
function Dashboard({ userId }: { userId: string }) {
const dataPromise = useMemo(
() => fetchDashboardData(userId),
[userId]
);
return (
<Suspense fallback={<DashboardSkeleton />}>
<DashboardContent dataPromise={dataPromise} />
</Suspense>
);
}
The trade-off here is that the entire dashboard waits for all three fetches to complete. If you want each section to appear independently, use separate Suspense boundaries with individual promises as shown in the earlier nested Suspense example.
Streaming with use() and Suspense
In a Server Components architecture, use() enables progressive streaming. The server can start sending HTML for resolved components while still waiting for slower data sources:
// Server Component - Next.js App Router
import { Suspense } from "react";
import Comments from "./Comments";
import ArticleBody from "./ArticleBody";
export default async function ArticlePage({
params,
}: {
params: { slug: string };
}) {
// This resolves fast - article content from CDN
const article = await fetchArticle(params.slug);
// This is slow - comments from database
const commentsPromise = fetchComments(params.slug);
return (
<main>
{/* This renders immediately with the article content */}
<ArticleBody article={article} />
{/* This streams in when comments are ready */}
<Suspense fallback={<CommentsSkeleton />}>
<Comments commentsPromise={commentsPromise} />
</Suspense>
</main>
);
}
// Client Component
"use client";
import { use } from "react";
export default function Comments({
commentsPromise,
}: {
commentsPromise: Promise<Comment[]>;
}) {
const comments = use(commentsPromise);
return (
<section>
<h2>Comments ({comments.length})</h2>
{comments.map((comment) => (
<div key={comment.id} className="comment">
<strong>{comment.author}</strong>
<p>{comment.body}</p>
</div>
))}
</section>
);
}
The server sends the article body HTML immediately. When the comments promise resolves, the server streams in the comments HTML, and React swaps out the skeleton on the client. The user sees the article content instantly without waiting for the slower database query.
Optimistic UI with useOptimistic + use()
React 19’s useOptimistic pairs well with use() for forms that read server data:
"use client";
import { use, useOptimistic, useActionState } from "react";
interface Todo {
id: string;
text: string;
completed: boolean;
}
function TodoList({
todosPromise,
addTodoAction,
}: {
todosPromise: Promise<Todo[]>;
addTodoAction: (formData: FormData) => Promise<void>;
}) {
const todos = use(todosPromise);
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
todos,
(state, newTodoText: string) => [
...state,
{ id: `temp-${Date.now()}`, text: newTodoText, completed: false },
]
);
async function handleSubmit(formData: FormData) {
const text = formData.get("text") as string;
addOptimisticTodo(text);
await addTodoAction(formData);
}
return (
<div>
<ul>
{optimisticTodos.map((todo) => (
<li key={todo.id} className={todo.id.startsWith("temp-") ? "pending" : ""}>
{todo.text}
</li>
))}
</ul>
<form action={handleSubmit}>
<input name="text" placeholder="New todo..." required />
<button type="submit">Add</button>
</form>
</div>
);
}
Combining use() with useActionState for Forms
useActionState (renamed from useFormState in the React 19 stable release) manages form submission state. Combined with use(), you can build forms that both read server data and handle submissions cleanly:
"use client";
import { use, useActionState, Suspense } from "react";
interface Profile {
name: string;
email: string;
bio: string;
}
function ProfileForm({
profilePromise,
updateAction,
}: {
profilePromise: Promise<Profile>;
updateAction: (prev: any, formData: FormData) => Promise<{ success: boolean; error?: string }>;
}) {
const profile = use(profilePromise);
const [state, formAction, isPending] = useActionState(updateAction, {
success: false,
});
return (
<form action={formAction}>
<label>
Name
<input name="name" defaultValue={profile.name} disabled={isPending} />
</label>
<label>
Email
<input
name="email"
type="email"
defaultValue={profile.email}
disabled={isPending}
/>
</label>
<label>
Bio
<textarea name="bio" defaultValue={profile.bio} disabled={isPending} />
</label>
<button type="submit" disabled={isPending}>
{isPending ? "Saving..." : "Save Changes"}
</button>
{state.error && <p className="error">{state.error}</p>}
{state.success && <p className="success">Profile updated.</p>}
</form>
);
}
Preloading Data with cache()
In Server Components, React’s cache() function deduplicates requests so multiple components can call the same data function without triggering multiple fetches:
import { cache } from "react";
import { use, Suspense } from "react";
// cache() deduplicates this call within a single server request
const getUser = cache(async (id: string): Promise<User> => {
const res = await fetch(`https://api.example.com/users/${id}`);
return res.json();
});
// Server Component - Layout
export default async function UserLayout({
params,
children,
}: {
params: { id: string };
children: React.ReactNode;
}) {
// Both this component and its children can call getUser(params.id)
// and only ONE fetch request will be made
const user = await getUser(params.id);
return (
<div>
<UserNav user={user} />
{children}
</div>
);
}
// Server Component - Page (same user, no duplicate fetch)
export default async function UserSettingsPage({
params,
}: {
params: { id: string };
}) {
const user = await getUser(params.id);
return <SettingsForm user={user} />;
}
Server Components and use()
When to Use async/await vs use()
The key distinction is simple:
- Server Components can be
asyncfunctions. Useawaitdirectly. - Client Components cannot be
asyncfunctions. Useuse()with Suspense.
// SERVER COMPONENT - use async/await directly
export default async function ServerUserProfile({ id }: { id: string }) {
const user = await fetchUser(id);
const posts = await fetchUserPosts(id);
return (
<div>
<h1>{user.name}</h1>
<PostList posts={posts} />
</div>
);
}
// CLIENT COMPONENT - use use() with Suspense
"use client";
import { use } from "react";
export default function ClientUserProfile({
userPromise,
postsPromise,
}: {
userPromise: Promise<User>;
postsPromise: Promise<Post[]>;
}) {
const user = use(userPromise);
const posts = use(postsPromise);
return (
<div>
<h1>{user.name}</h1>
<PostList posts={posts} />
</div>
);
}
Passing Promises Across the Server/Client Boundary
Next.js and other React frameworks serialize Promises across the server/client boundary. When a Server Component passes a Promise as a prop to a Client Component, the framework handles serialization transparently:
// Server Component
import { Suspense } from "react";
import InteractiveChart from "./InteractiveChart";
export default function AnalyticsPage() {
// This Promise is created on the server
const dataPromise = fetchAnalyticsData();
return (
<div>
<h1>Analytics</h1>
{/* The Promise is serialized and passed to the client */}
<Suspense fallback={<ChartSkeleton />}>
<InteractiveChart dataPromise={dataPromise} />
</Suspense>
</div>
);
}
// Client Component - needs interactivity (hover, zoom, click handlers)
"use client";
import { use, useState } from "react";
export default function InteractiveChart({
dataPromise,
}: {
dataPromise: Promise<AnalyticsData>;
}) {
const data = use(dataPromise);
const [hoveredPoint, setHoveredPoint] = useState<DataPoint | null>(null);
return (
<div
className="chart"
onMouseMove={(e) => {
const point = findNearestDataPoint(data, e);
setHoveredPoint(point);
}}
>
<svg viewBox="0 0 800 400">
{data.points.map((point) => (
<circle
key={point.id}
cx={point.x}
cy={point.y}
r={hoveredPoint?.id === point.id ? 8 : 4}
fill={hoveredPoint?.id === point.id ? "#ff6b6b" : "#339af0"}
/>
))}
</svg>
{hoveredPoint && (
<div className="tooltip">
{hoveredPoint.label}: {hoveredPoint.value}
</div>
)}
</div>
);
}
The important detail: the fetch starts on the server before any client-side JavaScript loads. By the time the browser downloads and hydrates the Client Component, the data may already be available. This eliminates the traditional client-side waterfall where you had to: download JS, parse JS, render component, start fetch, wait for response, re-render with data.
Other New React 19 Hooks
React 19 shipped several other new APIs alongside use(). Here is a brief overview of how they complement it.
useActionState
useActionState manages the lifecycle of a form action. It tracks pending state, the last returned result, and provides a form action function:
const [state, formAction, isPending] = useActionState(serverAction, initialState);
Use it alongside use() when building forms that need to both read server data (with use()) and submit updates (with useActionState).
useFormStatus
useFormStatus reads the status of a parent <form> from within its children. This is useful for building reusable submit buttons that show loading state:
import { useFormStatus } from "react-dom";
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? "Submitting..." : "Submit"}
</button>
);
}
useOptimistic
useOptimistic lets you show an optimistic UI state while an async action is in progress. When combined with use() for initial data loading, it creates a seamless read-and-write experience:
const [optimisticMessages, addOptimisticMessage] = useOptimistic(
messages,
(state, newMessage) => [...state, { ...newMessage, sending: true }]
);
How They All Fit Together
These APIs form a cohesive data story in React 19:
use()reads data during render (initial load)useActionStatehandles form submissions (writes)useOptimisticprovides instant feedback during writesuseFormStatusenables loading indicators in nested form components
Together, they replace the fragmented combination of useEffect + useState + custom loading/error state + manual optimistic updates that was necessary before React 19.
Testing Components That Use use()
Testing with React Testing Library
Components that use use() need to be wrapped in Suspense during testing, just as they are in production:
import { render, screen } from "@testing-library/react";
import { Suspense } from "react";
import UserProfile from "./UserProfile";
test("renders user name after loading", async () => {
const userPromise = Promise.resolve({
id: "1",
name: "Jane Doe",
email: "jane@example.com",
});
render(
<Suspense fallback={<div>Loading...</div>}>
<UserProfile userPromise={userPromise} />
</Suspense>
);
// The fallback may briefly appear
expect(await screen.findByText("Jane Doe")).toBeInTheDocument();
});
Testing Loading States
To test the Suspense fallback, use a Promise that you control when to resolve:
test("shows loading skeleton while data is pending", async () => {
let resolvePromise: (value: User) => void;
const userPromise = new Promise<User>((resolve) => {
resolvePromise = resolve;
});
render(
<Suspense fallback={<div data-testid="skeleton">Loading...</div>}>
<UserProfile userPromise={userPromise} />
</Suspense>
);
// Fallback should be visible while Promise is pending
expect(screen.getByTestId("skeleton")).toBeInTheDocument();
// Resolve the Promise
await act(async () => {
resolvePromise({
id: "1",
name: "Jane Doe",
email: "jane@example.com",
});
});
// Now the actual content should appear
expect(screen.getByText("Jane Doe")).toBeInTheDocument();
expect(screen.queryByTestId("skeleton")).not.toBeInTheDocument();
});
Testing Error Boundaries
To test error handling, pass a rejected Promise and verify the Error Boundary renders:
test("shows error message when fetch fails", async () => {
const failedPromise = Promise.reject(new Error("Network error"));
// Suppress React error boundary console.error in test output
const consoleSpy = jest.spyOn(console, "error").mockImplementation(() => {});
render(
<ErrorBoundary fallback={<div>Something went wrong</div>}>
<Suspense fallback={<div>Loading...</div>}>
<UserProfile userPromise={failedPromise} />
</Suspense>
</ErrorBoundary>
);
expect(await screen.findByText("Something went wrong")).toBeInTheDocument();
consoleSpy.mockRestore();
});
Testing Conditional Context with use()
When testing components that conditionally read context with use(), provide the context in your test wrapper:
test("reads admin context when isAdmin is true", async () => {
const adminConfig = { secretDashboardUrl: "/admin/secret" };
render(
<AdminContext.Provider value={adminConfig}>
<ThemeContext.Provider value="dark">
<FeaturePanel isAdmin={true} />
</ThemeContext.Provider>
</AdminContext.Provider>
);
expect(screen.getByText(/Secret key/)).toBeInTheDocument();
});
test("does not read admin context when isAdmin is false", async () => {
render(
<ThemeContext.Provider value="light">
<FeaturePanel isAdmin={false} />
</ThemeContext.Provider>
);
expect(screen.getByText("User Panel")).toBeInTheDocument();
expect(screen.queryByText(/Secret key/)).not.toBeInTheDocument();
});
Best Practices
Keep Promises Stable
The most important rule for use() is that the Promise you pass to it must be referentially stable across re-renders. If a new Promise object is created every render, React treats it as a new resource and re-suspends the component.
Create promises in Server Components (they run once):
// Server Component - runs once, Promise is stable
export default function Page() {
const dataPromise = fetchData(); // stable
return (
<Suspense fallback={<Loading />}>
<ClientComponent dataPromise={dataPromise} />
</Suspense>
);
}
Use useMemo for Client Component promise creation:
function ClientWrapper({ id }: { id: string }) {
// Only creates a new Promise when `id` changes
const dataPromise = useMemo(() => fetchData(id), [id]);
return (
<Suspense fallback={<Loading />}>
<DataDisplay dataPromise={dataPromise} />
</Suspense>
);
}
Use route loaders to create promises outside the component lifecycle entirely.
Use Suspense Boundaries Strategically
Too few Suspense boundaries means a single slow fetch blocks your entire page. Too many creates a chaotic loading experience where dozens of skeletons pop in independently.
A good heuristic:
- One Suspense boundary per independent content section. A dashboard with revenue, orders, and analytics should have three boundaries.
- Group tightly coupled data under one boundary. A user header that shows both name and avatar should not have separate boundaries for each.
- Place Suspense at layout boundaries. Sidebar, main content, and footer are natural boundary points.
Provide Meaningful Loading Fallbacks
Skeleton screens that match the layout of the loaded content are far better than generic spinners. They reduce perceived loading time and prevent layout shift:
<Suspense fallback={<ArticleSkeleton />}>
<ArticleContent articlePromise={articlePromise} />
</Suspense>
Build skeleton components that match the dimensions and structure of the real content. Tools like react-content-loader can help, or you can build simple CSS-based skeletons with pulsing animations.
Error Boundaries at Appropriate Granularity
Mirror your Suspense boundary structure with Error Boundaries. Each independent section should have its own Error Boundary so that one failed fetch does not take down the entire page:
<ErrorBoundary fallback={<SectionError section="revenue" />}>
<Suspense fallback={<RevenueSkeleton />}>
<RevenueChart revenuePromise={revenuePromise} />
</Suspense>
</ErrorBoundary>
Include retry mechanisms in your Error Boundary fallbacks. A simple “Retry” button that resets the error state and triggers a new fetch goes a long way for user experience.
Prefer Framework-Level Data Fetching
If you are using Next.js, Remix, React Router, or TanStack Start, use their built-in data loading primitives (loaders, server actions, generateMetadata) rather than rolling your own caching and fetch management. These frameworks handle cache invalidation, revalidation, and prefetching in ways that are difficult to replicate correctly in userland.
use() is the mechanism that makes these framework features work. You are often better off using use() indirectly through your framework’s data layer than calling it directly with hand-crafted promises.
Common Mistakes
Creating Promises Inside the Component Body
This is mistake number one and it causes infinite re-renders:
// BUG: new Promise every render -> infinite suspension loop
function Products() {
const products = use(fetchProducts()); // fetchProducts() creates a new Promise
return <ProductGrid products={products} />;
}
The fix: create the Promise in a parent component, a route loader, a Server Component, or wrap it in useMemo.
Missing Suspense Boundary
If there is no <Suspense> boundary above a component that calls use() with a Promise, the suspension propagates up the tree until it hits one. If there is no Suspense boundary anywhere, React will throw an error.
In practice, the most common symptom is that a far-away Suspense boundary (like one at your application root) catches the suspension, causing the entire app to show a loading spinner when only one small component is fetching data.
// BUG: No Suspense boundary - the entire app's root boundary will catch this
function App() {
return (
<div>
<Header />
{/* This will suspend up to the nearest ancestor Suspense */}
<UserProfile userPromise={userPromise} />
<Footer />
</div>
);
}
// FIX: Add a local Suspense boundary
function App() {
return (
<div>
<Header />
<Suspense fallback={<ProfileSkeleton />}>
<UserProfile userPromise={userPromise} />
</Suspense>
<Footer />
</div>
);
}
Missing Error Boundary
Without an Error Boundary, a rejected Promise from use() will crash your entire application. Always pair Suspense boundaries with Error Boundaries in production:
// BUG: rejected promise will crash the app
<Suspense fallback={<Loading />}>
<DataComponent dataPromise={dataPromise} />
</Suspense>
// FIX: Error Boundary catches rejected promises
<ErrorBoundary fallback={<ErrorMessage />}>
<Suspense fallback={<Loading />}>
<DataComponent dataPromise={dataPromise} />
</Suspense>
</ErrorBoundary>
Using use() for Subscriptions
use() is designed for one-time reads of a Promise or Context. It is not a subscription mechanism. If you need to react to data that changes over time (WebSocket messages, real-time database updates, browser events), use useEffect with useState, or use a library like useSyncExternalStore:
// WRONG: use() reads a Promise once, it does not subscribe to changes
function LivePrice({ symbol }: { symbol: string }) {
const price = use(fetchPrice(symbol)); // only gets the initial price
return <span>{price}</span>;
}
// RIGHT: useEffect + useState for subscriptions
function LivePrice({ symbol }: { symbol: string }) {
const [price, setPrice] = useState<number | null>(null);
useEffect(() => {
const ws = new WebSocket(`wss://prices.example.com/${symbol}`);
ws.onmessage = (event) => setPrice(JSON.parse(event.data).price);
return () => ws.close();
}, [symbol]);
return <span>{price ?? "Loading..."}</span>;
}
Not Handling Race Conditions
When using use() with client-side promise creation, be aware that fast navigation between different items can cause stale data to appear if you do not invalidate previous promises:
// POTENTIAL BUG: if userId changes rapidly, stale promises may resolve out of order
function UserWrapper({ userId }: { userId: string }) {
const userPromise = useMemo(() => fetchUser(userId), [userId]);
return (
<Suspense fallback={<Loading />}>
<UserDisplay userPromise={userPromise} />
</Suspense>
);
}
In most cases, React handles this correctly because each re-render with a new userId creates a new promise via useMemo, and React’s Suspense mechanism discards the previous suspended render. However, for complex scenarios with nested suspense boundaries or transitions, consider using useTransition to manage the handoff:
function UserWrapper({ userId }: { userId: string }) {
const [isPending, startTransition] = useTransition();
const userPromise = useMemo(() => fetchUser(userId), [userId]);
return (
<div className={isPending ? "opacity-50" : ""}>
<Suspense fallback={<Loading />}>
<UserDisplay userPromise={userPromise} />
</Suspense>
</div>
);
}
Conclusion
React 19’s use() API represents a genuine shift in how React applications handle asynchronous data and context. By integrating with Suspense and Error Boundaries, it eliminates the boilerplate that has plagued React data fetching for years and provides a declarative model where components describe what data they need rather than how to fetch it.
The key takeaways:
use()reads Promises and Context during render. It is an API, not a hook, and can be called in conditionals and loops.- Promises must be stable. Create them in Server Components, route loaders, or
useMemo— never inside the render body of the component that callsuse(). - Suspense handles loading, Error Boundaries handle errors. This separation of concerns keeps component code clean and moves UI state management to the component tree structure.
use()complements, not replaces, the existing ecosystem.useEffectis still the right tool for subscriptions and side effects. TanStack Query is still the right tool for complex cache management.use()is the right tool for one-time data reads at render time.- Frameworks make
use()shine. The full power ofuse()comes when paired with Server Components and framework-level data loading in Next.js, Remix, or React Router.
If you are starting a new React 19 project, make use() your default approach for data fetching. If you are migrating an existing codebase, start by identifying useEffect-based data fetches that can be lifted to route loaders or Server Components, and migrate those first. The result will be cleaner, faster, and more maintainable code.
Further Reading
Frequently Asked Questions
What is the use() hook in React 19?
use() is a new React 19 API that lets you read the value of a resource (Promise or Context) during render. Unlike other hooks, use() can be called inside conditionals and loops, making it more flexible for conditional data fetching and context consumption.
How is use() different from useEffect for data fetching?
useEffect fetches data after render (causing loading states and waterfalls), while use() integrates with Suspense to fetch data during render. With use(), you pass a Promise to the component and React suspends rendering until the data is ready, showing a Suspense fallback automatically.
Can I call use() inside an if statement?
Yes. Unlike useState, useEffect, and other hooks that must be called at the top level, use() can be called inside conditionals, loops, and after early returns. This makes it uniquely flexible for conditional data dependencies.
Does use() replace useContext?
use() can read Context like useContext does, with the added benefit of being callable inside conditionals. However, useContext still works and is not deprecated. use() is preferred when you need conditional context access.
Explore More
Related Articles
- Fake SOC 2 and ISO 27001 Certifications Are Spreading Across Dev Tools
- Input vs Output vs Reasoning Tokens Cost - LLM Pricing Explained
- MISRA C:2012 Rules with Examples - Complete Guide for Embedded Developers
- Parallel Tool Calling in LLM Agents - Complete Guide with Code Examples
- ripgrep vs grep: Performance Benchmarks and Why AI Agents Use rg
Free Newsletter
Stay ahead with AI dev tools
Weekly insights on AI code review, static analysis, and developer productivity. No spam, unsubscribe anytime.
Join developers getting weekly AI tool insights.
Related Articles
Fake SOC 2 and ISO 27001 Certifications Are Spreading Across Dev Tools
A recent investigation alleges that compliance automation platform Delve manufactured false SOC 2 and ISO 27001 certifications for startups. Here is what developers should know and how to verify the tools you trust.
March 20, 2026
guideInput vs Output vs Reasoning Tokens Cost - LLM Pricing Explained
Understand the difference between input, output, and reasoning tokens in LLMs. Compare pricing for GPT-4o, Claude, Gemini, and o3 models with cost optimization tips for AI code review.
March 20, 2026
guideMISRA C:2012 Rules with Examples - Complete Guide for Embedded Developers
Learn MISRA C:2012 rules with practical C code examples. Covers mandatory, required, and advisory rules, violations vs compliant code, and the best MISRA compliance tools.
March 20, 2026