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.
Quick take:
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.
How Does use() With Context Handle What useContext() Can't?
// 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}`} />;
}
Restructuring the first example to call useContext before the early return would work too, so the win isn't huge on its own. But in a component with several conditional branches each needing different context values, use() avoids restructuring the whole function just to satisfy hook ordering. Per the React docs, use() is explicitly exempt from the Rules of Hooks that govern useState, useEffect, and useContext, the three most commonly used hooks in a typical component tree. That exemption is intentional rather than an oversight: React's own reference documentation calls out use() by name as the one API in the hooks family designed to be called conditionally, inside loops, and after early returns.
Where Does use() With Promises Actually Matter?
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. A promise reader is a hook that unwraps a value from a promise someone else created, rather than a hook that initiates the fetch, caches the result, or dedupes repeated requests for the same data. Two other libraries, SWR and React Query, already solve caching and deduplication, so pairing use() with one of them, instead of a raw inline fetch(), is the pattern React's own team recommends in the reference docs.
Why Is the Rule-Breaking Necessary Here?
Call-order tracking is React's internal mechanism for matching each hook invocation in one render to the same hook invocation in the next render, purely by position rather than by name. 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.
Why Is Creating a New Promise Every Render a Mistake?
// 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.
How to check whether a component is safe to pair with use():
- Confirm the promise passed to
use()is created outside the render function that callsuse(), not inline. - Trace where the promise comes from: a Server Component, a route loader, or a caching library like SWR or React Query.
- If the promise is created with a plain
fetch()call inside the same component, refactor it to a cached, stable reference before shipping. - Verify with React DevTools that the component only re-suspends when the underlying data actually changes, not on every render.
How Does use() Compare to useEffect for Data Fetching?
Four practical differences separate the two approaches, and each one shows up the first time a component actually ships to production rather than in a toy example. Loading and error states go from hand-rolled state variables with useEffect to boundary components that handle both declaratively with use(). Waterfalls, where each effect fires only after its parent has mounted, are common with useEffect and largely avoidable with use() when the promise starts earlier, often in a Server Component that begins fetching before the client even renders.
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.