React 19 use API: Promises, Context & Suspense
A complete beginner-friendly guide to the React 19 use API. Learn what use() is, why it was introduced, how to use it with Promises and Context, how it integrates with Suspense, and how it compares to useEffect and useContext.
React 19 is one of the most significant releases in the library's history, and one of its headline additions is the use() API. At first glance it looks like just another hook, but it behaves differently from anything React has shipped before. It can read the value of a Promise or a Context object — and it can do so conditionally, inside loops, or after early returns. If you have ever wrestled with useEffect for data fetching or felt the verbosity of useContext, use() is worth your full attention.
What Is the use() API?
use() is a new React function (technically called a "hook-like" API) that lets a component suspend while waiting for a Promise to resolve, or read a Context value — all in a single, readable call. Unlike traditional hooks, use() is not bound by the Rules of Hooks in the same way: you can call it inside conditionals and loops, which is a first for React.
Here is the simplest mental model: when you pass a Promise to use(), React pauses rendering that component until the Promise settles. When you pass a Context object to use(), React returns the current context value — just like useContext, but with more flexibility.
import { use } from 'react';
// Reading a Context with use()
const theme = use(ThemeContext);
// Reading a Promise with use()
const user = use(fetchUserPromise);use() is the first React API that can be called conditionally. It is not a hook — it is a new primitive that works inside both Client and Server Components.
Why Was use() Introduced? The Problem It Solves
Before use(), fetching data in React required a dance between useEffect, useState, and loading/error flags. Even with libraries like React Query or SWR, the pattern was verbose. Server Components helped on the server side, but client-side async logic remained clunky.
Context had its own friction: useContext had to be called at the top level of a component, making conditional context reads impossible without restructuring your component tree.
- Eliminate boilerplate loading/error state for async data in Client Components.
- Allow Context to be read conditionally — useful for optional features or feature flags.
- Provide a single, unified API for both Promises and Context.
- Work seamlessly with React Suspense and Error Boundaries.
- Reduce the cognitive overhead of managing async lifecycle manually.
How to Use use() with Promises and Suspense
When you pass a Promise to use(), React integrates with Suspense automatically. The component "suspends" — React pauses its render and shows the nearest <Suspense> fallback — until the Promise resolves. Once it resolves, React re-renders the component with the resolved value. If the Promise rejects, the nearest Error Boundary catches the error.
The key rule: the Promise must be created outside the component (or be stable across renders). Creating a new Promise on every render would cause an infinite loop of suspending. Typically you create the Promise at the module level, pass it as a prop, or use a caching layer.
// userService.ts — create the promise OUTSIDE the component
export const userPromise = fetch('/api/user').then((res) => res.json());// UserProfile.tsx
import { use, Suspense } from 'react';
import { userPromise } from './userService';
function UserProfile() {
// use() suspends until userPromise resolves
const user = use(userPromise);
return (
<div>
<h2>Welcome, {user.name}!</h2>
<p>Email: {user.email}</p>
</div>
);
}
// Wrap with Suspense so React knows what to show while loading
export default function App() {
return (
<Suspense fallback={<p>Loading user...</p>}>
<UserProfile />
</Suspense>
);
}Notice there is no useState, no useEffect, and no isLoading flag. The component reads the resolved value directly, and Suspense handles the loading state declaratively. Add an Error Boundary above the Suspense to handle fetch failures gracefully.
// Adding an Error Boundary for rejected Promises
import { ErrorBoundary } from 'react-error-boundary';
export default function App() {
return (
<ErrorBoundary fallback={<p>Failed to load user.</p>}>
<Suspense fallback={<p>Loading user...</p>}>
<UserProfile />
</Suspense>
</ErrorBoundary>
);
}How to Use use() with Context
use() can replace useContext entirely. The difference is that use() can be called conditionally — inside an if statement, a loop, or after an early return. This unlocks patterns that were previously impossible with useContext.
// ThemeContext.ts
import { createContext } from 'react';
export const ThemeContext = createContext<'light' | 'dark'>('light');// ThemedButton.tsx
import { use } from 'react';
import { ThemeContext } from './ThemeContext';
function ThemedButton({ showTheme }: { showTheme: boolean }) {
// ✅ Conditional use() — this is allowed!
if (showTheme) {
const theme = use(ThemeContext);
return <button className={theme}>Click me</button>;
}
return <button>Click me</button>;
}With useContext, the above pattern would break the Rules of Hooks. With use(), it is perfectly valid. This is especially useful for optional theming, feature flags, or any context that only certain branches of a component need.
use() vs useEffect and use() vs useContext
Understanding when to reach for use() versus the older APIs is critical. Here is a clear comparison:
| Feature | use() | useEffect | useContext |
| Reads a Promise | ✅ Yes | ⚠️ Indirectly (with setState) | ❌ No |
| Reads Context | ✅ Yes | ❌ No | ✅ Yes |
| Can be called conditionally | ✅ Yes | ❌ No | ❌ No |
| Integrates with Suspense | ✅ Automatic | ❌ Manual | ❌ No |
| Handles loading state | ✅ Via Suspense | ⚠️ Manual useState | ❌ N/A |
| Handles errors | ✅ Via Error Boundary | ⚠️ Manual try/catch | ❌ N/A |
| Runs after render | ❌ No (during render) | ✅ Yes | ❌ No (during render) |
The key insight: use() runs during rendering, not after it. useEffect is a side-effect escape hatch that runs after the DOM is painted. They solve different problems. Use use() when you want to read async data or context during render. Use useEffect when you need to interact with the DOM, set up subscriptions, or run code after the component mounts.
// ❌ Old pattern — useEffect + useState for data fetching
function OldUserProfile() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch('/api/user')
.then((res) => res.json())
.then(setUser)
.catch(setError)
.finally(() => setLoading(false));
}, []);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error!</p>;
return <p>Hello, {user.name}</p>;
}
// ✅ New pattern — use() with Suspense
const userPromise = fetch('/api/user').then((r) => r.json());
function NewUserProfile() {
const user = use(userPromise);
return <p>Hello, {user.name}</p>;
}Rules, Limitations, and Common Mistakes
use() is flexible, but it has its own set of rules and gotchas you need to know before shipping it to production.
- Only call use() inside a React component or a custom hook — not in event handlers, utility functions, or class components.
- The Promise passed to use() must be stable. Do NOT create a new Promise inside the component body on every render — this causes an infinite suspend loop.
- Always wrap components that use use(Promise) in a <Suspense> boundary, or React will throw an error.
- Always pair Suspense with an Error Boundary to handle rejected Promises — otherwise unhandled rejections will crash the component tree.
- use() does not cache Promises. If you need deduplication or caching, use a library (React Query, SWR) or a module-level cache.
- use() cannot be used in Server Components to read client-only Context — Context is a client-side concept.
The most common mistake beginners make is creating the Promise inside the component:
// ❌ WRONG — new Promise created on every render = infinite loop
function BadComponent() {
const data = use(fetch('/api/data').then((r) => r.json())); // 🚫
return <div>{data.title}</div>;
}
// ✅ CORRECT — Promise created once, outside the component
const dataPromise = fetch('/api/data').then((r) => r.json());
function GoodComponent() {
const data = use(dataPromise);
return <div>{data.title}</div>;
}Another common mistake is forgetting the Suspense wrapper. If a component calls use(promise) and there is no Suspense ancestor, React throws an error in development and the app crashes in production.
Practical Real-World Example: Dynamic Data with Props
A realistic pattern is to create the Promise at the parent level (or in a router loader), pass it as a prop, and let the child component read it with use(). This keeps data-fetching concerns at the top of the tree while keeping components clean.
// postsService.ts
export function fetchPost(id: string): Promise<{ title: string; body: string }> {
return fetch(`/api/posts/${id}`).then((res) => {
if (!res.ok) throw new Error('Post not found');
return res.json();
});
}// PostPage.tsx — parent creates the promise
import { Suspense } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { fetchPost } from './postsService';
import PostDetail from './PostDetail';
export default function PostPage({ postId }: { postId: string }) {
// Promise is created here — stable as long as postId doesn't change
const postPromise = fetchPost(postId);
return (
<ErrorBoundary fallback={<p>Could not load post.</p>}>
<Suspense fallback={<p>Loading post...</p>}>
{/* Pass the promise as a prop */}
<PostDetail postPromise={postPromise} />
</Suspense>
</ErrorBoundary>
);
}// PostDetail.tsx — child reads the promise with use()
import { use } from 'react';
type Post = { title: string; body: string };
export default function PostDetail({
postPromise,
}: {
postPromise: Promise<Post>;
}) {
const post = use(postPromise); // suspends until resolved
return (
<article>
<h1>{post.title}</h1>
<p>{post.body}</p>
</article>
);
}This pattern — sometimes called "Promise as prop" — is idiomatic React 19. It separates the concern of when to fetch (parent) from how to display (child), and it composes naturally with Suspense and Error Boundaries.
When Should You Use use()? Decision Guide
use() is powerful, but it is not always the right tool. Here is a quick decision guide:
- ✅ Use use(Promise) when you have a stable Promise (from a prop, module, or router loader) and want clean, declarative data reading with Suspense.
- ✅ Use use(Context) when you need to read Context conditionally or inside a loop — something useContext cannot do.
- ⚠️ Prefer React Query or SWR when you need caching, background refetching, pagination, or mutation handling — use() has no built-in cache.
- ⚠️ Stick with useEffect when you need to run side effects after render: DOM manipulation, subscriptions, timers, or analytics calls.
- ❌ Avoid use() in Server Components for client-only Context — Context does not exist on the server.
- ❌ Avoid use() for Promises that change frequently without a caching layer — you will trigger excessive Suspense fallbacks.
Frequently Asked Questions (FAQ)
Below are the most common questions developers have when learning the use() API.
- Q: Is use() a hook? — Technically, no. React calls it a "hook-like" API or just a "function." It does not follow the same rules as hooks (it can be called conditionally), but it can only be used inside components or custom hooks.
- Q: Does use() work in React 18? — No. use() is a React 19 feature. In React 18, you can achieve similar patterns with libraries like SWR or React Query.
- Q: Can I use use() in a Server Component? — You can use use(Promise) in Server Components. However, you cannot use use(Context) in Server Components because Context is a client-side concept.
- Q: What happens if the Promise rejects? — React propagates the error to the nearest Error Boundary. Always wrap Suspense with an Error Boundary when using use(Promise).
- Q: Does use() cache the Promise result? — No. use() does not cache. If the same Promise reference is passed again, React reuses the result. But if a new Promise is created, React will suspend again. Use a caching layer for production data fetching.
- Q: Can I use use() inside a custom hook? — Yes! You can build custom hooks that internally call use(), just like you can with useState or useEffect.
- Q: Is use() better than useContext? — For most cases, use() is a drop-in replacement for useContext with the added benefit of conditional calls. There is no performance difference.
Next Steps and Further Learning
The use() API is a foundational building block for the future of React data fetching and context consumption. Here is how to continue your learning journey:
- Read the official React 19 release notes and the use() reference in the React docs.
- Experiment with use(Promise) in a small Next.js 15 or Vite project to feel how Suspense and Error Boundaries compose.
- Explore React Query v5's integration with React 19 — it exposes a useSuspenseQuery hook that pairs perfectly with use() patterns.
- Learn about React Server Components and how they handle async data without use() at all — using async/await directly in the component body.
- Study the Suspense API more deeply: nested Suspense boundaries, startTransition, and how they affect perceived performance.
React 19's use() API is a genuine quality-of-life improvement. It removes boilerplate, makes async data reading declarative, and unlocks conditional Context reads. Start small — replace one useContext call or one useEffect data fetch — and you will quickly see why the React team considers use() one of the most important additions to the library in years.