React 19 use() API: Read Promises and Context Directly During Render
Learn how React 19's use() API lets components read Promises and Context directly during render. This practical guide explains how it works with Suspense, data fetching, Context, and real-world use cases.
Introduction
React developers have traditionally used Hooks such as:
useState
useEffect
useContextto manage data and component behavior.
For asynchronous data, a common pattern looked like this:
useEffect(() => {
fetchData();
}, []);For Context, we normally used:
const value = useContext(MyContext);React 19 introduces a new API:
use()The use() API can read:
Promises
Context
directly during rendering.
That may sound like a small change, but it introduces a very different way to think about async data in React.
A simple example looks like:
const data = use(dataPromise);or:
const theme = use(ThemeContext);In this article, we will understand how use() works, where it is useful, how it interacts with Suspense, and how to test it with practical examples.
What Problem Does use() Solve?
Let's start with asynchronous data.
Before React 19, client-side data fetching often looked like this:
import {
useEffect,
useState,
} from "react";
function UserProfile() {
const [user, setUser] =
useState(null);
const [loading, setLoading] =
useState(true);
const [error, setError] =
useState(null);
useEffect(() => {
fetch("/api/user")
.then((res) => res.json())
.then((data) => {
setUser(data);
})
.catch((error) => {
setError(error);
})
.finally(() => {
setLoading(false);
});
}, []);
if (loading) {
return <p>Loading...</p>;
}
if (error) {
return <p>Something went wrong.</p>;
}
return (
<h1>
{user.name}
</h1>
);
}This works.
But notice how much state we manage manually:
user
loading
errorThe component is also responsible for triggering the request and updating all of those states.
React's newer async rendering model moves toward a different approach:
Data Promise
↓
use()
↓
Suspense
↓
Rendered UIInstead of manually checking:
loading = trueReact can suspend the component until the Promise is ready.
What Is use()?
Import it from React:
import { use } from "react";A Promise can be read like this:
const result = use(promise);If the Promise is still pending, React suspends the component.
If it resolves, use() returns the resolved value.
If it rejects, the error is thrown to the nearest error boundary.
Conceptually:
Promise Pending
↓
use()
↓
Suspend Component
↓
Show Suspense FallbackWhen resolved:
Promise Resolved
↓
use()
↓
Return Value
↓
Render ComponentFirst Important Rule
Do not think of use() as:
await inside JSXIt is integrated with React's rendering system.
React needs to control what happens while the Promise is pending.
That is why use() works naturally with:
<Suspense />What We Will Build
We will build a simple user dashboard.
The flow will be:
App
↓
Create User Promise
↓
Suspense
↓
UserProfile
↓
use(userPromise)
↓
Display UserWe will also test:
Pending Promise
Successful Promise
Failed Promise
Context with
use()Conditional Context reading
Common mistakes
Step 1: Create a React 19 Project
Create a Vite project:
npm create vite@latest react-use-api-demoChoose:
React
JavaScriptEnter the project:
cd react-use-api-demoInstall dependencies:
npm installStart:
npm run devUsually:
http://localhost:5173Step 2: Confirm React 19
Run:
npm list react react-domMake sure React 19 or later is installed.
Your dependencies should look similar to:
{
"dependencies": {
"react": "^19.x.x",
"react-dom": "^19.x.x"
}
}Step 3: Create a Fake API Promise
Create:
src/api/user.jsAdd:
export function getUser() {
return new Promise((resolve) => {
setTimeout(() => {
resolve({
id: 1,
name: "Jalish Mahmud",
role: "Software Engineer",
});
}, 2000);
});
}This simulates a two-second API request.
Step 4: Create the Promise Outside the Component
In:
src/App.jsxadd:
import {
Suspense,
} from "react";
import UserProfile
from "./UserProfile";
import {
getUser,
} from "./api/user";
const userPromise =
getUser();
function App() {
return (
<main>
<h1>
React 19 use() Demo
</h1>
<Suspense
fallback={
<p>
Loading user...
</p>
}
>
<UserProfile
userPromise={
userPromise
}
/>
</Suspense>
</main>
);
}
export default App;The important part is:
const userPromise =
getUser();This Promise is created once.
Then we pass it down.
Step 5: Read the Promise with use()
Create:
src/UserProfile.jsxAdd:
import {
use,
} from "react";
export default function UserProfile({
userPromise,
}) {
const user =
use(userPromise);
return (
<section>
<h2>
{user.name}
</h2>
<p>
{user.role}
</p>
</section>
);
}This line is the key:
const user =
use(userPromise);There is no:
useEffectNo:
useStateNo:
loadingNo:
setUserReact reads the Promise directly.
Step 6: Test the Pending State
Run:
npm run devOpen:
http://localhost:5173For approximately two seconds, you should see:
Loading user...Then:
Jalish Mahmud
Software EngineerThe flow is:
Render UserProfile
↓
use(userPromise)
↓
Promise pending
↓
Component suspends
↓
Suspense fallback
↓
Promise resolves
↓
Component rendersThis is one of the most important concepts behind use().
What Does Suspense Do Here?
Our component tries to read:
use(userPromise)But the Promise is not finished.
React temporarily suspends that part of the UI.
The parent contains:
<Suspense
fallback={
<p>Loading user...</p>
}
>So React displays the fallback.
Once the Promise resolves, React retries rendering the component.
Now:
use(userPromise)returns the resolved value.
Step 7: Test Faster and Slower Requests
Change:
setTimeout(() => {to:
setTimeout(() => {with:
5000For example:
export function getUser() {
return new Promise((resolve) => {
setTimeout(() => {
resolve({
id: 1,
name: "Jalish Mahmud",
role: "Software Engineer",
});
}, 5000);
});
}Now the Suspense fallback remains visible for around five seconds.
This helps you clearly see how suspension works.
use() vs useEffect
A common question is:
Is
use()replacinguseEffect?
No.
They solve different problems.
useEffect is designed for side effects.
Examples:
Subscriptions
DOM synchronization
Analytics
Timers
Browser APIs
External systemsuse() is for reading supported resources during render.
For example:
Promise
ContextThink about it like this:
Need to read async data?
→ use()
Need to synchronize with external system?
→ useEffect()Traditional Data Fetching Flow
A common client-side pattern is:
Render
↓
useEffect runs
↓
Fetch data
↓
setState
↓
Render againWith a Promise and use():
Render
↓
Read Promise
↓
Suspend if pending
↓
Promise resolves
↓
Render dataThat is a fundamentally different model.
Step 8: Simulate an Error
Now modify:
src/api/user.jsto:
export function getUser() {
return new Promise(
(
resolve,
reject
) => {
setTimeout(() => {
reject(
new Error(
"Failed to load user"
)
);
}, 2000);
}
);
}The Promise now rejects.
When:
use(userPromise)reads a rejected Promise, the error is thrown during rendering.
To handle that properly, use an Error Boundary.
Step 9: Create an Error Boundary
Create:
src/ErrorBoundary.jsxAdd:
import React from "react";
export default class ErrorBoundary
extends React.Component {
constructor(props) {
super(props);
this.state = {
hasError: false,
};
}
static getDerivedStateFromError() {
return {
hasError: true,
};
}
render() {
if (
this.state.hasError
) {
return (
<p>
Unable to load user.
</p>
);
}
return this.props.children;
}
}Step 10: Wrap the Suspense Boundary
Update:
function App() {
return (
<main>
<h1>
React 19 use() Demo
</h1>
<ErrorBoundary>
<Suspense
fallback={
<p>
Loading user...
</p>
}
>
<UserProfile
userPromise={
userPromise
}
/>
</Suspense>
</ErrorBoundary>
</main>
);
}Now refresh.
You should first see:
Loading user...Then:
Unable to load user.This demonstrates the complete async lifecycle.
Promise Lifecycle with use()
There are three important Promise states.
Pending
Promise pending
↓
use()
↓
Suspense fallbackFulfilled
Promise resolved
↓
use()
↓
Resolved valueRejected
Promise rejected
↓
use()
↓
Error BoundaryThis is a useful mental model.
Step 11: Use a Real API
Now let's replace the fake Promise with a real API request.
Create:
export async function getUser() {
const response =
await fetch(
"https://jsonplaceholder.typicode.com/users/1"
);
if (!response.ok) {
throw new Error(
"Unable to fetch user"
);
}
return response.json();
}Then:
const userPromise =
getUser();still works exactly the same way.
Our component does not need to change:
const user =
use(userPromise);That separation is useful.
Important: Avoid Creating a New Promise Every Render
A common mistake is:
function UserProfile() {
const user =
use(getUser());
return (
<p>
{user.name}
</p>
);
}This may create a new Promise each time the component renders.
That can cause unnecessary repeated suspension and requests.
A better approach is to create or cache the Promise outside the render path when appropriate.
For our simple demo:
const userPromise =
getUser();Then pass it:
<UserProfile
userPromise={
userPromise
}
/>Server Components and use()
use() becomes especially interesting when working with frameworks that support React Server Components.
For example, a server may create a Promise and pass it to a client component.
Conceptually:
Server Component
↓
Create Promise
↓
Pass Promise
↓
Client Component
↓
use()This can allow data loading to begin earlier while the UI is still being streamed.
This pattern is especially relevant in modern frameworks such as Next.js.
Another Important Use: Reading Context
use() is not only for Promises.
It can also read React Context.
Traditionally, we use:
const theme =
useContext(ThemeContext);With React 19:
const theme =
use(ThemeContext);At first, these may look very similar.
The interesting difference is that use() can be used conditionally.
Step 12: Create a Theme Context
Create:
src/ThemeContext.jsAdd:
import {
createContext,
} from "react";
export const ThemeContext =
createContext("light");Step 13: Provide the Context
Update:
import {
ThemeContext,
} from "./ThemeContext";
function App() {
return (
<ThemeContext
value="dark"
>
<Dashboard />
</ThemeContext>
);
}React 19 allows rendering the context object itself as a provider.
You may also see the traditional provider syntax in existing codebases.
Step 14: Read Context with use()
Create:
src/Dashboard.jsxAdd:
import {
use,
} from "react";
import {
ThemeContext,
} from "./ThemeContext";
export default function Dashboard() {
const theme =
use(ThemeContext);
return (
<div>
Current theme:
{theme}
</div>
);
}Expected:
Current theme: darkWhy Not Just Use useContext?
For simple cases, useContext remains completely valid.
For example:
const theme =
useContext(ThemeContext);There is nothing wrong with that.
The interesting part of use() is how it interacts with control flow.
Conditional Context Reading
Traditional Hooks generally follow the Rules of Hooks.
For example, you should not do:
if (showTheme) {
const theme =
useContext(ThemeContext);
}Hooks are expected to be called consistently.
But use() has different rules.
You can write:
function ThemeInfo({
showTheme,
}) {
if (!showTheme) {
return (
<p>
Theme hidden
</p>
);
}
const theme =
use(ThemeContext);
return (
<p>
Theme: {theme}
</p>
);
}That is one of the notable differences.
use() Can Be Called Conditionally
For example:
function Message({
show,
}) {
if (!show) {
return null;
}
const value =
use(MyContext);
return (
<p>
{value}
</p>
);
}This gives developers more flexibility in some component structures.
Important Limitation
Even though use() is more flexible than traditional Hooks, it still belongs inside a React component or Hook.
Do not treat it like a normal utility function that can be called anywhere.
It participates in React rendering.
Practical Use Case 1: User Profile
Imagine:
User Dashboard
↓
User Promise
↓
use()
↓
Profile UIThis works well when the Promise already exists and the component only needs to read its value.
Practical Use Case 2: Product Details
Example:
function Product({
productPromise,
}) {
const product =
use(productPromise);
return (
<>
<h1>
{product.name}
</h1>
<p>
${product.price}
</p>
</>
);
}Wrapped in:
<Suspense
fallback={
<ProductSkeleton />
}
>This creates a clean loading experience.
Practical Use Case 3: Dashboard Widgets
Imagine several independent widgets:
User Profile
Orders
Notifications
AnalyticsEach could potentially suspend independently.
For example:
<Suspense
fallback={<UserSkeleton />}
>
<UserProfile
userPromise={
userPromise
}
/>
</Suspense>and:
<Suspense
fallback={<OrdersSkeleton />}
>
<Orders
ordersPromise={
ordersPromise
}
/>
</Suspense>Now each part of the page can become ready independently.
Practical Use Case 4: Context Only When Needed
Imagine an optional settings panel.
function SettingsPanel({
enabled,
}) {
if (!enabled) {
return null;
}
const settings =
use(SettingsContext);
return (
<SettingsView
settings={
settings
}
/>
);
}The context is only read when that branch is rendered.
Practical Use Case 5: Authentication Context
function UserMenu() {
const auth =
use(AuthContext);
if (!auth.user) {
return (
<LoginButton />
);
}
return (
<p>
Welcome,
{auth.user.name}
</p>
);
}This looks familiar, but the same API can now also read async resources.
That unified model is one reason use() is interesting.
use() vs useContext
useContext
Use it when:
You simply need context
You prefer the established Hook
No conditional read is neededExample:
const user =
useContext(UserContext);use()
Use it when:
You want to read a Promise
You want to read Context conditionally
You are using Suspense-based data flowsExample:
const user =
use(UserContext);or:
const user =
use(userPromise);use() vs useEffect
Consider data fetching.
useEffect
Render component
↓
Effect starts
↓
Fetch data
↓
Update state
↓
Render againuse()
Read Promise
↓
Suspend
↓
Show fallback
↓
Promise resolves
↓
Render dataThese are different models.
use() vs async/await
Another common question:
Why not simply make every component async?
Client Components are not generally used like normal async JavaScript functions.
React needs to coordinate:
Rendering
Suspense
Streaming
Error boundaries
Retry behavior
use() allows React to participate in that process.
So:
const data =
use(dataPromise);is not just a syntax replacement for:
await dataPromise;Building a Better Loading UI
Instead of:
fallback={
<p>Loading...</p>
}you can create a skeleton.
For example:
function UserSkeleton() {
return (
<div>
<div>
Loading name...
</div>
<div>
Loading role...
</div>
</div>
);
}Then:
<Suspense
fallback={
<UserSkeleton />
}
>
<UserProfile
userPromise={
userPromise
}
/>
</Suspense>This improves perceived performance.
Multiple Suspense Boundaries
You do not need one loading state for the whole page.
For example:
<Suspense
fallback={
<ProfileSkeleton />
}
>
<Profile />
</Suspense>
<Suspense
fallback={
<PostsSkeleton />
}
>
<Posts />
</Suspense>The profile can appear before the posts if its data resolves first.
Conceptually:
Page
├── Profile
│ ↓
│ Suspense
│
└── Posts
↓
SuspenseThis allows progressive rendering.
Simulating Different Response Times
This is a useful exercise.
Create:
const userPromise =
new Promise((resolve) => {
setTimeout(() => {
resolve({
name: "Jalish",
});
}, 1000);
});Then:
const postsPromise =
new Promise((resolve) => {
setTimeout(() => {
resolve([
"Post 1",
"Post 2",
]);
}, 5000);
});Wrap them in separate Suspense boundaries.
Expected:
After 1 second
→ User appears
After 5 seconds
→ Posts appearThis is a simple way to understand progressive UI rendering.
Common Mistake 1: No Suspense Boundary
You write:
const data =
use(dataPromise);but there is no nearby Suspense fallback.
The user experience may not behave the way you expect.
A better structure is:
<Suspense
fallback={
<Loading />
}
>
<Component />
</Suspense>Common Mistake 2: Recreating the Promise
Avoid repeatedly doing:
use(fetchData());if that function creates a fresh Promise every render.
Prefer stable or cached Promises.
Common Mistake 3: Using use() for Every Async Operation
Not every asynchronous operation belongs in rendering.
For example:
Send analytics
Write localStorage
Subscribe to WebSocket
Manipulate DOMThese are side effects.
They still belong in patterns such as:
useEffectuse() is not a replacement for every Hook.
Common Mistake 4: Forgetting Error Handling
Suspense handles pending Promises.
It does not replace error handling.
Think of the architecture as:
Promise Pending
→ Suspense
Promise Rejected
→ Error Boundary
Promise Resolved
→ ComponentYou usually need both:
Suspense Boundary
+
Error Boundaryfor a complete async experience.
Test Checklist
Use this checklist after building the demo.
Test 1 — Pending Promise
Set delay:
5000 msExpected:
Loading user...for five seconds.
Test 2 — Resolved Promise
Expected:
User name
User roleafter the Promise finishes.
Test 3 — Rejected Promise
Reject:
new Error(
"Failed to load user"
)Expected:
Error Boundary UITest 4 — Real HTTP API
Replace the fake Promise with:
fetch(...)Expected:
Real API data renderedTest 5 — Separate Suspense Boundaries
Use:
1-second user Promise
5-second posts PromiseExpected:
User appears first
Posts appear laterTest 6 — Context
Read:
use(ThemeContext)Expected:
Provided theme valueTest 7 — Conditional Context
Render:
showTheme = falseExpected:
No Context readChange:
showTheme = trueExpected:
Context value displayedFinal Mental Model
For Promises:
Promise
↓
use()
↓
┌───────────────┐
│ Pending │
│ → Suspense │
├───────────────┤
│ Resolved │
│ → Value │
├───────────────┤
│ Rejected │
│ → Error │
│ Boundary │
└───────────────┘For Context:
Context
↓
use()
↓
Context ValueThat is the easiest way to remember the API.
Where use() Fits in React 19
React 19's newer APIs start to work together.
For example:
useActionState
→ Manage Action state
useFormStatus
→ Read form submission state
useOptimistic
→ Show immediate optimistic UI
use()
→ Read Promise or Context during renderEach solves a different problem.
Together, they support React's more modern async and Action-driven programming model.
Before and After
Traditional client-side async state:
useState
+
useEffect
+
loading
+
error
+
dataA Suspense-based flow can become:
Promise
+
use()
+
Suspense
+
Error BoundaryThis is not automatically better for every situation.
But for architectures designed around Suspense and async resources, it can dramatically simplify components.
Should You Replace Existing Code with use()?
Not automatically.
If your application currently uses:
React Query
SWR
Apollo
Redux Toolkit Querythose libraries already solve much more than simply reading Promises.
They may provide:
Caching
Refetching
Deduplication
Pagination
Mutation handling
Background updates
Query invalidation
use() does not automatically replace those features.
It is a React primitive, not a complete data-fetching library.
Use it where it fits the architecture.
Conclusion
React 19's use() API introduces a different way to read async resources and Context during rendering.
The syntax is simple:
const value =
use(resource);If the resource is a Promise:
Pending
→ Suspend
Resolved
→ Return value
Rejected
→ Throw errorIf the resource is a Context:
Context
→ Return current valueThe most important ideas to remember are:
use()can read Promises.It works naturally with Suspense.
Promise errors should be handled with an Error Boundary.
use()can also read Context.Unlike traditional Hooks,
use()can be used in conditional control flow.It does not replace
useEffect.It does not replace full data-fetching libraries.
Avoid creating unstable Promises during rendering.
The best way to understand use() is to build a small application and simulate:
Slow request
Successful request
Failed request
Multiple Suspense boundaries
Context reading
Conditional renderingOnce those examples make sense, React's newer async rendering model becomes much easier to understand.