Every hooks tutorial opens with the same rule: never call a hook inside a condition, a loop, or after an early return, because React tracks hook state by call order, and changing the order between renders corrupts that tracking. use() breaks that rule, deliberately, and the fact that it's allowed to is the entire reason it exists as a separate API instead of just being folded into useContext or a new useAsync.
TL;DR:
use()reads a promise or a context value, and unlike every other hook, it can be called conditionally, inside loops, or after early returns. Reading a promise withuse()suspends the component, working with a<Suspense>boundary the same wayReact.lazydoes, and a rejected promise is caught by the nearest error boundary. It doesn't fetch data itself, it reads a promise someone else created.
use() With Context: The Conditional Case useContext() Can't Handle
// This is invalid, useContext() can't follow an early return
function Avatar({ userId }) {
if (!userId) {return null;}
const theme = useContext(ThemeContext); // React throws: hook called conditionally
return <img className={theme.avatarClass} src={`/avatars/${userId}`} />;
}
// use() is fine here, because it isn't bound by the Rules of Hooks
import { use } from 'react';
function Avatar({ userId }) {
if (!userId) {return null;}
const theme = use(ThemeContext); // valid, even after the early return
return <img className={theme.avatarClass} src={`/avatars/${userId}`} />;
}
This isn't a huge win on its own, you could restructure the first example to call useContext before the early return. But in a component with several conditional branches each needing different context values, use() avoids restructuring the whole function just to satisfy hook ordering.
use() With Promises: Where It Actually Matters
The more consequential use case is reading a promise directly inside a component, which suspends the component until the promise resolves:
import { use, Suspense } from 'react';
function UserProfile({ userPromise }) {
const user = use(userPromise); // suspends until the promise resolves
return <h1>{user.name}</h1>;
}
function App({ userPromise }) {
return (
<Suspense fallback={<Spinner />}>
<UserProfile userPromise={userPromise} />
</Suspense>
);
}
Notice UserProfile never calls fetch() itself, use() reads a promise that was passed in as a prop, created somewhere above (often in a Server Component, or a route loader). That's a deliberate design choice: use() is a promise reader, not a data-fetching library. It doesn't cache, dedupe, or manage the promise's lifecycle, that's still the job of whatever created it.
Why the Rule-Breaking Is Necessary Here
Regular hooks track state by call order because React needs to associate the third useState call in render N with the third useState call in render N+1, and skip that association entirely if a hook call is conditionally missing. use() has no persistent state to track across renders in the same way, it either suspends (if the promise is pending), throws (if it rejected), or returns a value (if it resolved), evaluated fresh against whatever promise or context is passed in on that specific render. There's no call-order bookkeeping to corrupt, which is exactly what makes the conditional usage safe.
Common Mistake: Creating a New Promise Every Render
// Wrong: creates a new promise on every render, re-suspending forever
function UserProfile({ userId }) {
const user = use(fetchUser(userId)); // new promise each render
return <h1>{user.name}</h1>;
}
// Right: the promise is created once, outside the render that reads it
// (e.g., in a Server Component, or cached with a library like SWR/React Query)
function UserProfilePage({ userId }) {
const userPromise = getCachedUserPromise(userId); // stable reference
return (
<Suspense fallback={<Spinner />}>
<UserProfile userPromise={userPromise} />
</Suspense>
);
}
function UserProfile({ userPromise }) {
const user = use(userPromise);
return <h1>{user.name}</h1>;
}
Calling fetchUser(userId) directly inside the component that calls use() creates a brand-new promise every render, which means every render suspends again waiting for a fresh fetch, an infinite loading loop. The promise has to be created once and kept stable across renders, which is why use() is usually paired with a Server Component or a data-fetching library that caches promises by key, not called against a raw fetch() inline.
use() vs useEffect for Data Fetching
useEffect + fetch | use() + Suspense | |
|---|---|---|
| Loading state | Manual isLoading state | <Suspense fallback> handles it |
| Error state | Manual try/catch + state | Nearest error boundary catches it |
| Waterfalls | Common, each effect fires after mount | Avoided if promises start earlier (e.g. in a Server Component) |
| Can be conditional | N/A, the effect itself isn't the issue | Yes, use() can follow an early return |
Conclusion
use() earns its exemption from the Rules of Hooks by not needing the call-order tracking that makes those rules necessary in the first place. It's most useful paired with Suspense for reading a promise created elsewhere, and secondarily for reading context in places useContext() can't reach because of an early return or conditional. It's not a fetch() replacement, it's a way to read a promise someone else's code produced.