Skip to main content

React's `use` Hook: Reading Promises Inside Components

The use hook, introduced in React 19, is the primary way to read promises (and contexts) inside components. It unwraps a promise and returns its resolved value synchronously. If the promise is still pending, use throws it to the nearest Suspense boundary, which displays a fallback. This single mechanism eliminates the need for manual promise state management in your components.

Unlike older approaches, use integrates seamlessly with React's concurrent rendering. It lets you write asynchronous-aware components without useEffect or .then() chains.

How use Works: Three States

The use hook handles all three promise states: pending, fulfilled, and rejected.

Pending: When you call use(promise) and the promise is not yet resolved, use throws the promise itself. React catches this throw, pauses the component's render, and displays the nearest Suspense fallback. This is not an error; it's a signal to React's rendering system.

Fulfilled: Once the promise resolves, use returns the resolved value. React retries the component's render, use now returns synchronously (no throw), and the component renders with the data.

Rejected: If the promise rejects, use throws the error. It bubbles to the nearest Error Boundary, not to Suspense. This separation lets you handle loading and errors independently.

Here's a minimal example showing all three:

import { use, Suspense } from 'react';

function DataDisplay({ promise }) {
// If promise is pending: use() throws, Suspense catches
// If promise is fulfilled: use() returns the data
// If promise is rejected: use() throws the error to Error Boundary
const data = use(promise);

return <div>{data.name}</div>;
}

export function App() {
const promise = fetch('/api/data').then(r => r.json());

return (
<Suspense fallback={<div>Loading...</div>}>
<DataDisplay promise={promise} />
</Suspense>
);
}

When DataDisplay mounts, use(promise) either throws the promise (if pending) or returns the data (if resolved). The Suspense boundary handles the pending case by showing the fallback.

Promise Caching and Re-renders

A critical detail: use expects the same promise instance across renders. If you create a new promise on every render, use will keep throwing, and Suspense will stay in fallback forever. Always create promises outside the component or memoize them:

// Wrong: new promise every render
function BadComponent() {
const promise = fetch('/api/data').then(r => r.json()); // ❌ new promise
const data = use(promise); // Always throws
return <div>{data}</div>;
}

// Correct: same promise instance
function GoodComponent({ promise }) {
const data = use(promise); // Uses the promise passed in
return <div>{data}</div>;
}

export function Parent() {
const promise = fetch('/api/data').then(r => r.json()); // ✅ created once
return (
<Suspense fallback={<div>Loading...</div>}>
<GoodComponent promise={promise} />
</Suspense>
);
}

In GoodComponent, the promise is created in the parent and passed as a prop. The same instance is used across all renders, so use will eventually return the resolved value.

Using use with Multiple Promises

When a component needs multiple pieces of async data, call use for each:

import { use, Suspense } from 'react';

function UserProfile({ userPromise, followersPromise, postsPromise }) {
// All three promises are unwrapped here
const user = use(userPromise);
const followers = use(followersPromise);
const posts = use(postsPromise);

return (
<div>
<h1>{user.name}</h1>
<p>Followers: {followers.length}</p>
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
);
}

export function UserPage({ userId }) {
// All fetches start immediately
const userPromise = fetch(`/api/users/${userId}`).then(r => r.json());
const followersPromise = fetch(`/api/users/${userId}/followers`).then(r => r.json());
const postsPromise = fetch(`/api/users/${userId}/posts`).then(r => r.json());

return (
<Suspense fallback={<div>Loading profile...</div>}>
<UserProfile
userPromise={userPromise}
followersPromise={followersPromise}
postsPromise={postsPromise}
/>
</Suspense>
);
}

React's concurrent rendering waits for all promises to resolve before rendering the component. This avoids waterfalls: all three requests happen in parallel, then the component renders once.

Combining use with Context

The use hook also unwraps Context values, which is useful for async context (contexts that might be promises):

import { createContext, use } from 'react';

const DataContext = createContext(null);

function DataConsumer() {
// If the context value is a promise, use() unwraps it
const data = use(DataContext);
return <div>{data.value}</div>;
}

export function Provider({ children, dataPromise }) {
return (
<DataContext.Provider value={dataPromise}>
{children}
</DataContext.Provider>
);
}

This pattern is rare but useful for libraries that provide async context.

Handling Errors from use

When a promise passed to use rejects, the error is thrown from the component and bubbles to the nearest Error Boundary. Suspense does not catch these errors:

import { use, Suspense } from 'react';

class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { error: null };
}

static getDerivedStateFromError(error) {
return { error };
}

render() {
if (this.state.error) {
return <div>Error: {this.state.error.message}</div>;
}
return this.props.children;
}
}

function DataDisplay({ promise }) {
const data = use(promise); // If promise rejects, throws error
return <div>{data}</div>;
}

export function App({ dataPromise }) {
return (
<ErrorBoundary>
<Suspense fallback={<div>Loading...</div>}>
<DataDisplay promise={dataPromise} />
</Suspense>
</ErrorBoundary>
);
}

If the promise rejects, the error skips Suspense and is caught by ErrorBoundary. This lets you show different UI for loading (Suspense) vs. errors (Error Boundary).

Comparing use to .then() and async/await

use is fundamentally different from async/await or promise chains:

// Old: async/await (can't be used directly in components)
async function fetchData() {
const data = await fetch('/api/data').then(r => r.json());
return data; // Not a promise; the actual data
}

// Old: promise chain
function fetchData() {
return fetch('/api/data')
.then(r => r.json())
.then(data => { /* handle */ });
}

// New: use hook (designed for components)
function MyComponent({ promise }) {
const data = use(promise); // Always returns data (or throws)
// Rendering logic here
}

use is unique because it's designed for React's rendering model. It lets React pause and resume rendering based on promise state, enabling Suspense. Traditional async/await requires all promise handling to happen before rendering (outside the component), which is less flexible.

Real-World Example: Search Results

Here's a practical example combining use with a search input:

import { use, Suspense, startTransition, useState } from 'react';

function SearchResults({ resultsPromise }) {
const results = use(resultsPromise);

return (
<ul>
{results.map(item => (
<li key={item.id}>{item.title}</li>
))}
</ul>
);
}

export function SearchPage() {
const [query, setQuery] = useState('');
const [resultsPromise, setResultsPromise] = useState(null);

function handleSearch(newQuery) {
setQuery(newQuery);
startTransition(() => {
// Create a new promise and update state
const promise = fetch(`/api/search?q=${newQuery}`)
.then(r => r.json());
setResultsPromise(promise);
});
}

return (
<div>
<input
type="text"
value={query}
onChange={e => handleSearch(e.target.value)}
placeholder="Search..."
/>
{resultsPromise && (
<Suspense fallback={<div>Searching...</div>}>
<SearchResults resultsPromise={resultsPromise} />
</Suspense>
)}
</div>
);
}

When the user types, startTransition initiates a new search request, updates the promise, and Suspense shows "Searching..." while results load.

Key Takeaways

  • use unwraps promises: Call use(promise) inside a component; if pending, it throws to Suspense; if resolved, it returns the data.
  • Promise identity matters: Use the same promise instance across renders; create promises outside the component or memoize.
  • use handles three states: Pending (throws to Suspense), fulfilled (returns data), rejected (throws to Error Boundary).
  • Multiple promises work in parallel: Call use for each promise in a component; React waits for all to resolve before rendering.
  • Errors bubble separately: Rejected promises throw errors that skip Suspense and go to Error Boundary.

Frequently Asked Questions

Can I use use with any promise?

Yes, any promise works: fetch, library promises (like axios), or custom promises you create. As long as you pass the promise instance to use, it will handle all three states correctly.

What if I call use conditionally?

Never call use conditionally. It must be called at the same place in every render (like all hooks). If data is optional, initialize the promise to a resolved value (Promise.resolve(null)) and check the result:

function MyComponent({ optionalPromise = Promise.resolve(null) }) {
const data = use(optionalPromise); // Always called in the same place
if (data === null) return <div>No data</div>;
return <div>{data}</div>;
}

Does use work in Server Components?

Yes. In Next.js Server Components, you can pass promises directly, and use will handle them. Suspense boundaries wrap the component, and streaming renders the fallback first, then the data.

Can I update a promise that's already being used?

Creating a new promise and passing it to a component causes a re-render. The component will throw the new promise to Suspense, show the fallback, and resume once the new promise resolves. Use startTransition to prevent blocking the UI during this transition.

What's the difference between use and useEffect + useState?

useEffect + useState requires manual state management (loading, error, data). use automatically handles this via Suspense and Error Boundary. use is simpler and enables concurrent rendering; useEffect blocks rendering and doesn't support Suspense.

Further Reading