Skip to content

use() & Data Fetching

use() is a React 19 API that reads the value of a resource during render — most importantly a promise or a context. When you pass it a promise, the component suspends while the promise is pending (showing the nearest Suspense fallback) and returns the resolved value once it settles.

import { use, Suspense } from "react";
function Message({ messagePromise }) {
const text = use(messagePromise); // suspends until the promise resolves
return <p>{text}</p>;
}
function Container({ messagePromise }) {
return (
<Suspense fallback={<p>Loading…</p>}>
<Message messagePromise={messagePromise} />
</Suspense>
);
}

use() breaks one rule you learned for other hooks: it can be called conditionally — inside an if, or in a loop. That’s allowed precisely because it isn’t tied to the fixed hook slots the way useState/useEffect are.

use() does not start the fetch, and it does not cache anything. It reads a promise you give it — so that promise must be stable across renders. Creating a promise inside the component body means a new promise every render, which suspends forever (or refetches endlessly).

// ❌ New promise every render — refetches / never settles.
function Bad({ id }) {
const data = use(fetch(`/api/${id}`).then(r => r.json()));
// ...
}
// ✅ The promise is created outside and passed in (cached by the caller/framework).
function Good({ dataPromise }) {
const data = use(dataPromise);
// ...
}

This is why raw use() is almost always paired with a cache — either a framework’s data layer or a manual Map cache — that returns the same promise instance for the same request.

The pattern use() enables is render-as-you-fetch: kick off the request before or as you render (e.g. in a route loader, or on hover), pass the promise down, and let the component read it via use() inside a Suspense boundary. Contrast the three approaches:

flowchart TB
  subgraph old["fetch-on-render (useEffect)"]
    r1["render"] --> e1["effect runs"] --> f1["then fetch starts"] --> r2["re-render with data"]
  end
  subgraph new["render-as-you-fetch (use + Suspense)"]
    start["start fetch early
(loader / event)"] --> pass["pass promise down"]
    pass --> read["use(promise) suspends
→ Suspense fallback → data"]
  end
Fetch-on-render vs render-as-you-fetch

use() also reads context, and unlike useContext it can be called conditionally — handy for reading context inside a branch.

Here’s the honest part. Raw use() + Suspense + a hand-rolled promise cache is powerful but low-level. In real apps you almost always get render-as-you-fetch through a framework (a router/loader that fetches per route and provides the cache) or a data library like TanStack Query (which manages caching, revalidation, and Suspense integration for you). Learn use() to understand the mechanism; reach for a library to ship it.

What happens when you pass a pending promise to `use()`?
How is `use()` different from hooks like `useState`?
Why must the promise passed to `use()` be cached / stable across renders?
In practice, how do most apps do render-as-you-fetch?