Stop writing useEffect.
For a decade, we have suffered in "Hook Hell." Accessing asynchronous data or global context meant abiding by the strict "Rules of Hooks." Top level only. No loops. No conditions.
React 19 breaks these chains with the universal use() API. It isn't just a hook; it's a Control Flow Primitive. It unwraps promises. It consumes context. And for the first time in React history: It runs inside loops and if-statements.
The use API represents the convergence of React's Server and Client stories. It is the bridge that allows data to flow from your backend (Promises) directly into your UI (Components) without the boilerplate of useState, useEffect, and manual loading states.
This guide covers everything from the basic syntax to advanced architectural patterns that are only possible with this new primitive.
02. Data Fetching: Unwrapping Promises
In React 18 and earlier, handling a promise meant managing three states: loading, error, and success.
With use(), you simply "ask" for the value. If the promise is pending, React Suspends the component. If it rejects, React hits the Error Boundary. If it resolves, execution continues.
The Old Way (Imperative)
function UserProfile({ id }) {
const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
let active = true;
setIsLoading(true);
fetchUser(id).then(u => {
if (active) {
setUser(u);
setIsLoading(false);
}
});
return () => { active = false; }
}, [id]);
if (isLoading) return <Spinner />;
return <div>{user.name}</div>;
}
The New Way (Declarative)
import { use } from 'react';
// The promise is passed IN. The component reads it.
// Loading is handled by <Suspense> in the parent.
function UserProfile({ userPromise }) {
const user = use(userPromise);
return <div>{user.name}</div>;
}
How it works under the hood
When use(promise) is called:
- If the promise is Pending: React throws the promise (literally throws it like an error). The nearest
<Suspense>boundary catches it and renders the fallback. - When the promise Resolves: React re-renders the component. This time,
use(promise)returns the resolved value immediately. - If the promise Rejects: React throws the error, which bubbles up to the nearest Error Boundary.
Deep Dive: Error Boundaries
Because rejecting promises are thrown as errors, Error Boundaries are no longer optional.
If you don't wrap a component that uses `use(promise)` in an Error Boundary, a single network failure will crash your entire application (White Screen of Death).
03. The Context Revolution
useContext is one of the most used hooks, but it has a major flaw: it must be called at the top level. This means you often subscribe to contexts that you might not even use, just in case.
With use(Context), you can read context only when you need it.
import { use } from 'react';
import { ThemeContext } from './theme';
function Header({ showSettings }) {
// 1. Standard Render
if (!showSettings) {
return <div>Welcome</div>;
}
// 2. Conditional Context Subscription
// This line only runs if showSettings is true.
// We save the cost of subscription when it's false.
const theme = use(ThemeContext);
return <div style={{ color: theme.color }}>Settings</div>;
}
Performance Win: In large apps, avoiding unnecessary context subscriptions can significantly reduce render checking overhead.
04. Conditional Hooks? Yes.
This breaks the Golden Rule of hooks: "Don't call hooks inside loops, conditions, or nested functions."
The use API is an exception. It is technically a function, not a "Hook" in the traditional sense (it doesn't maintain fiber state order in the same rigid way for storage, as it relies on the promise/context identity).
Loops
function UserList({ users }) {
return (
<ul>
{users.map(user => {
// ✅ Valid!
const details = use(user.detailsPromise);
return <li>{details.bio}</li>;
})}
</ul>
);
}
Early Returns
function Panel({ isExpanded }) {
if (!isExpanded) return null;
// ✅ Valid! Runs after an early return.
const data = use(somePromise);
return <div>{data}</div>;
}
05. Critical Pitfalls
Pitfall 1: Creating Promises during Render
Do NOT create a promise inside the component function body.
// ❌ WRONG - Infinite Loop Hazard
function Component() {
const promise = fetch('/api'); // Creates a NEW promise every render!
const data = use(promise); // Suspends. Re-renders. New Promise. Suspends...
}
// ✅ RIGHT
// Pass promise as prop, or create in a Server Component / External Cache
function Component({ dataPromise }) {
const data = use(dataPromise);
}
Pitfall 2: 'use' is for Reading, not Fetching
use() is designed to read a value that is already being fetched. It shouldn't trigger the fetch itself. The fetch should be initiated by:
- A Server Component (preferred)
- A library like TanStack Query
- An event handler
06. Migration Strategy 2026
You don't need to rewrite your entire app. Adopt use() iteratively.
- Start with Leaf Components: Convert small components that currently receive data props to instead receive Promises of data.
- Hoist Data Fetching: Move your
fetchcalls up to the parent or Server Component. Pass the promise down. - Replace Context Hooks: Identify components that conditionally render. Swap
useContextforuse(Context)and move the call inside the conditional block.
07. Try the API
The component below demonstrates use() mimicking a Suspense falback.