React.js

React 19 use() API: Read Promises and Context Directly During Render

React 19 use API example showing Promise data fetching with Suspense, resolved state, Error Boundary, and Context reading 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
useContext

to 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
error

The 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 UI

Instead of manually checking:

loading = true

React 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 Fallback

When resolved:

Promise Resolved
     ↓
use()
     ↓
Return Value
     ↓
Render Component

First Important Rule

Do not think of use() as:

await inside JSX

It 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 User

We 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-demo

Choose:

React
JavaScript

Enter the project:

cd react-use-api-demo

Install dependencies:

npm install

Start:

npm run dev

Usually:

http://localhost:5173

Step 2: Confirm React 19

Run:

npm list react react-dom

Make 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.js

Add:

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.jsx

add:

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.jsx

Add:

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:

useEffect

No:

useState

No:

loading

No:

setUser

React reads the Promise directly.


Step 6: Test the Pending State

Run:

npm run dev

Open:

http://localhost:5173

For approximately two seconds, you should see:

Loading user...

Then:

Jalish Mahmud
Software Engineer

The flow is:

Render UserProfile
       ↓
use(userPromise)
       ↓
Promise pending
       ↓
Component suspends
       ↓
Suspense fallback
       ↓
Promise resolves
       ↓
Component renders

This 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:

5000

For 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() replacing useEffect?

No.

They solve different problems.

useEffect is designed for side effects.

Examples:

Subscriptions
DOM synchronization
Analytics
Timers
Browser APIs
External systems

use() is for reading supported resources during render.

For example:

Promise
Context

Think 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 again

With a Promise and use():

Render
 ↓
Read Promise
 ↓
Suspend if pending
 ↓
Promise resolves
 ↓
Render data

That is a fundamentally different model.


Step 8: Simulate an Error

Now modify:

src/api/user.js

to:

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.jsx

Add:

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 fallback

Fulfilled

Promise resolved
 ↓
use()
 ↓
Resolved value

Rejected

Promise rejected
 ↓
use()
 ↓
Error Boundary

This 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.js

Add:

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.jsx

Add:

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: dark

Why 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 UI

This 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
Analytics

Each 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 needed

Example:

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 flows

Example:

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 again

use()

Read Promise
 ↓
Suspend
 ↓
Show fallback
 ↓
Promise resolves
 ↓
Render data

These 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
       ↓
    Suspense

This 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 appear

This 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 DOM

These are side effects.

They still belong in patterns such as:

useEffect

use() 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
→ Component

You usually need both:

Suspense Boundary
+
Error Boundary

for a complete async experience.


Test Checklist

Use this checklist after building the demo.

Test 1 — Pending Promise

Set delay:

5000 ms

Expected:

Loading user...

for five seconds.


Test 2 — Resolved Promise

Expected:

User name
User role

after the Promise finishes.


Test 3 — Rejected Promise

Reject:

new Error(
  "Failed to load user"
)

Expected:

Error Boundary UI

Test 4 — Real HTTP API

Replace the fake Promise with:

fetch(...)

Expected:

Real API data rendered

Test 5 — Separate Suspense Boundaries

Use:

1-second user Promise
5-second posts Promise

Expected:

User appears first
Posts appear later

Test 6 — Context

Read:

use(ThemeContext)

Expected:

Provided theme value

Test 7 — Conditional Context

Render:

showTheme = false

Expected:

No Context read

Change:

showTheme = true

Expected:

Context value displayed

Final Mental Model

For Promises:

Promise
   ↓
use()
   ↓
┌───────────────┐
│ Pending       │
│ → Suspense    │
├───────────────┤
│ Resolved      │
│ → Value       │
├───────────────┤
│ Rejected      │
│ → Error       │
│   Boundary    │
└───────────────┘

For Context:

Context
   ↓
use()
   ↓
Context Value

That 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 render

Each 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
 +
data

A Suspense-based flow can become:

Promise
 +
use()
 +
Suspense
 +
Error Boundary

This 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 Query

those 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 error

If the resource is a Context:

Context
→ Return current value

The 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 rendering

Once those examples make sense, React's newer async rendering model becomes much easier to understand.