Skip to content

The use() Hook in React 19: Reading Promises and Context

Why use() Isn't a Hook Like the Others

React 19's use() reads promises and context inside conditionals and loops, breaking the Rules of Hooks on purpose. Here's what that trade-off buys you.

· · 5 min read

Quick Take

The Rules of Hooks are supposed to be non-negotiable, no calling hooks inside conditionals or loops. React 19's use() ignores that rule on purpose, and understanding why it's allowed to is the whole point of the API.

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 with use() suspends the component, working with a <Suspense> boundary the same way React.lazy does, 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 + fetchuse() + Suspense
Loading stateManual isLoading state<Suspense fallback> handles it
Error stateManual try/catch + stateNearest error boundary catches it
WaterfallsCommon, each effect fires after mountAvoided if promises start earlier (e.g. in a Server Component)
Can be conditionalN/A, the effect itself isn't the issueYes, 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.

Frequently Asked Questions

Is use() a React hook?
It's named like one and lives in the same import, but React's own docs are explicit that use() is not subject to the Rules of Hooks. Regular hooks (useState, useEffect) must be called unconditionally at the top level of a component, in the same order on every render. use() can be called inside if statements, loops, and after early returns, because unlike other hooks it doesn't rely on call-order-based internal state tracking.
What can use() read besides promises?
Context. use(SomeContext) reads a context value, the same job useContext() does, but use() can be called conditionally, inside an if block or after an early return, where useContext() cannot. For new code reading context conditionally, use() is the more flexible option; useContext() still works fine for the unconditional case.
What happens if a promise passed to use() rejects?
The nearest error boundary catches it, the same way a thrown error during render would be caught. This means promise rejection from use() needs an <ErrorBoundary> ancestor, the same requirement Suspense-based data fetching already has for its loading state via <Suspense>. Without one, a rejected promise crashes the whole render tree instead of showing a fallback UI.