Understanding the 'use' API in React 19: Conditional Resources and Promise Unwrapping
Since Hooks were introduced in React 16.8, one invariant has defined idiomatic React code: the Rules of Hooks. Hooks could never be called inside conditional statements, loops, or after early returns. This rule ensured that React's internal fiber queue preserved an identical order of Hook calls across consecutive renders.
With the release of React 19, React introduced use (React documentation), a brand-new primitive for consuming resources directly during render. While named with the familiar use prefix, it is technically an API rather than a standard Hook, bringing two groundbreaking capabilities: it can unwrap Promises within render via <Suspense>, and it can be called conditionally.
In this deep dive, we explore how use works, how it transforms Context and asynchronous streaming, common pitfalls, and architectural best practices.
What is the use API?
use accepts a resource—either a JavaScript Promise or a React Context—and returns its resolved value:
import { use } from 'react';
// Reading Context
const value = use(MyContext);
// Reading a Promise
const data = use(dataPromise);
How use Differs from Traditional Hooks
The defining distinction between use and standard React Hooks (like useState or useContext) is where it may be called:
- Conditional Invocations Allowed: Unlike
useContext, you can invokeuseinsideifstatements,switchblocks, loops, and after early returns. - Component or Hook Execution Only: Like Hooks,
usemust still be called inside a React component or custom Hook during render. It cannot be executed inside standard event handlers or asynchronous closures.
Reading Context Conditionally
Prior to React 19, reading Context required useContext. If a component only needed context under specific conditions, useContext still had to be invoked unconditionally at the top level of the component:
// Before React 19: Unconditional Context reading
function AlertBadge({ isVisible }) {
const theme = useContext(ThemeContext); // Always read, even when not visible
if (!isVisible) return null;
return <span className={theme}>Alert</span>;
}
With use, you can defer reading the context until after validation or condition checks:
// With React 19: Flexible conditional reading
import { use } from 'react';
function AlertBadge({ isVisible }) {
if (!isVisible) return null;
// Only reads from ThemeContext if isVisible is true
const theme = use(ThemeContext);
return <span className={theme}>Alert</span>;
}
If the nearest Context Provider changes value, components that never entered the conditional branch do not re-render, optimizing performance in deeply branched component hierarchies.
Unwrapping Promises and Streaming Data
The most transformative application of use is reading asynchronous Promises directly during render. When passed a pending Promise, use suspends component rendering (React documentation).
React catches the suspended promise and renders the nearest <Suspense> fallback UI until the promise settles. Once resolved, React re-renders the component with the resolved value.
Practical Example: Streaming Data from Server to Client
When paired with React Server Components (RSC) in frameworks like Next.js, Server Components can initiate data fetching without awaiting it, passing the raw Promise down to a Client Component:
// Server Component: App.tsx
import { Suspense } from 'react';
import { UserProfile } from './UserProfile';
async function fetchUserData(userId: string) {
const res = await fetch(`https://api.example.com/users/${userId}`);
return res.json();
}
export default function Page({ params }: { params: { id: string } }) {
// Kick off the Promise without blocking the initial shell render
const userPromise = fetchUserData(params.id);
return (
<main>
<h1>Account Details</h1>
<Suspense fallback={<p>Loading profile details...</p>}>
<UserProfile userPromise={userPromise} />
</Suspense>
</main>
);
}
// Client Component: UserProfile.tsx
'use client';
import { use } from 'react';
interface User {
id: string;
name: string;
email: string;
}
export function UserProfile({
userPromise,
}: {
userPromise: Promise<User>;
}) {
// Suspends until userPromise resolves
const user = use(userPromise);
return (
<div className="profile-card">
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}
Because the server streams the initial page shell immediately, user metrics like Time to First Byte (TTFB) and First Contentful Paint (FCP) improve significantly.
Error Handling with Error Boundaries
When a Promise passed to use rejects, React throws the rejection error up the tree to the nearest Error Boundary.
To provide a resilient user experience, always wrap components consuming promises in both an Error Boundary and a Suspense boundary:
import { use, Suspense } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
export function SafeUserContainer({ userPromise }: { userPromise: Promise<User> }) {
return (
<ErrorBoundary fallback={<p className="error">Failed to load user profile.</p>}>
<Suspense fallback={<p>Loading...</p>}>
<UserProfile userPromise={userPromise} />
</Suspense>
</ErrorBoundary>
);
}
Alternatively, you can catch rejections directly on the Promise before passing it to use using .catch() to provide fallback data.
The Critical Pitfall: Promises Created Inside Render
One of the most frequent mistakes developers make when first experimenting with use is instantiating a new Promise directly inside a client component body:
// ❌ WRONG: Creates an uncached Promise on every render
function BadComponent() {
const data = use(fetch('/api/data').then((res) => res.json()));
return <div>{data.title}</div>;
}
This triggers an explicit console error: "A component was suspended by an uncached promise."
Because every render re-creates the Promise, React enters an infinite re-render loop as the component perpetually suspends against a brand-new promise reference.
The Rule
Promises passed to use must be stable and cached. They should either:
- Originate from a Server Component and be passed as props.
- Come from a Suspense-compatible library (like React Query, SWR, or Relay).
- Be cached using an external cache or memoization utility.
Conclusion
The use API represents a fundamental shift in how React applications bridge asynchronous data and component rendering. By lifting the restriction on conditional calls and integrating first-class Promise resolution with Suspense, React 19 simplifies data flow and unlocks declarative streaming architectures across modern full-stack applications.