Skip to content

Why the React 19 use() Hook 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.

· · 7 min read
A computer screen showing lines of text

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.

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 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.

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.

A red and orange traffic light signaling stop, evoking a rule being broken on purpose
Photo by Maxim Abramov on Unsplash

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.

A small wooden hourglass with blue sand resting on a bed of stones
Photo by Aron Visuals on Unsplash

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():

  1. Confirm the promise passed to use() is created outside the render function that calls use(), not inline.
  2. Trace where the promise comes from: a Server Component, a route loader, or a caching library like SWR or React Query.
  3. If the promise is created with a plain fetch() call inside the same component, refactor it to a cached, stable reference before shipping.
  4. Verify with React DevTools that the component only re-suspends when the underlying data actually changes, not on every render.
Glowing blue light trails forming an infinite loop shape against a black background
Photo by Compare Fibre on Unsplash

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 + 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.