React use() Hook: Data Fetching in Server and Client Components
React's use() hook lands in React 19 and changes how you fetch data in both server and client components. Here's what it does, where it helps, and where it doesn't.
What use() Actually Is (And What It Isn't)
React 19 shipped use() as an API that reads the value of a resource — a Promise, a Context, or anything that fits that shape. It's not a replacement for useEffect. It's not a data-fetching library. It's a primitive that lets Suspense boundaries work the way they were always supposed to.
Here's the simplest version of the idea: you pass a Promise to use(), and React suspends the component until that Promise resolves. The nearest Suspense boundary shows a fallback in the meantime. No loading state variable. No manual catch logic scattered everywhere.
Worth noting: this only works inside React components or other hooks. You can't call use() in event handlers, inside setTimeout callbacks, or conditionally — well, actually you can call it conditionally, which is one of the things that makes it different from every other hook in React.
Quick aside: the ability to call use() inside an if-block is intentional. The React team explicitly broke with the Rules of Hooks for this one. That makes it feel more like a compiler-level primitive than a normal hook, which is kind of what it is.
Basic Usage with Promises
Let's look at a real example. Say you have a server action or API call returning a Promise, and you want to read its value inside a client component:
import { use, Suspense } from 'react';
type User = { id: number; name: string; email: string };
function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
const user = use(userPromise);
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}
export default function Page() {
const userPromise = fetch('/api/user/1').then(r => r.json());
return (
<Suspense fallback={<p>Loading user...</p>}>
<UserProfile userPromise={userPromise} />
</Suspense>
);
}The key move here is passing the Promise *down as a prop*, not awaiting it in the parent. The parent creates the Promise and hands it off. The child component calls use() on it and React handles the suspension. This pattern is sometimes called "promise streaming" because the server can start sending HTML immediately while the data loads.
In practice, the 32px skeleton spinner you'd normally show inside a useEffect-driven loading state can now be replaced with a proper Suspense boundary that integrates with React's rendering pipeline. The fallback UI is cleaner, the component is simpler, and — this matters — you get proper streaming with Next.js App Router.
One more thing — error handling. If the Promise rejects, use() will throw. You need an ErrorBoundary wrapping the same tree, or you'll get an uncaught error in production. Don't skip that step.
use() with Context: Conditional Reading
The other main use case is Context. Before React 19, you always had to call useContext at the top level of your component, unconditionally. With use(), you can read context inside if statements, loops, and early returns.
import { use } from 'react';
import { ThemeContext } from './ThemeContext';
function Button({ variant }: { variant?: 'ghost' | 'solid' }) {
// Conditionally reading context — this is now valid
if (variant === 'ghost') {
const theme = use(ThemeContext);
return <button style={{ color: theme.primary }}>Ghost</button>;
}
return <button>Solid</button>;
}Honestly, the Context use case matters more than it gets credit for in most articles. If you've ever had to restructure a component just to read a context value you only need in one code path, you know how frustrating that constraint is. use() removes it.
That said, it doesn't change the re-render semantics of Context. Every consumer still re-renders when the context value changes. If that's a performance concern for you, the answer is still splitting contexts or using a state manager — not use().
Server Components vs. Client Components: Where use() Fits
Here's where people get confused. In React Server Components (RSC), you can just await a Promise directly — no use() needed. RSC functions are async, so you write async/await the normal way.
// Server Component — no use() needed
export default async function ProductPage({ id }: { id: string }) {
const product = await fetchProduct(id); // plain await
return <h1>{product.title}</h1>;
}use() is for *client* components that receive a Promise they didn't create themselves, usually one passed from a parent Server Component. That's the whole streaming model in Next.js 15+ (released in late 2024). The server starts the fetch, wraps it in a Promise, passes it to the client component, and the client component reads it with use() inside a Suspense boundary.
// Server Component passes a Promise to a Client Component
import { Suspense } from 'react';
import { ClientCard } from './ClientCard'; // 'use client'
export default async function Page() {
// Don't await — pass the Promise directly
const dataPromise = fetchHeavyData();
return (
<Suspense fallback={<Skeleton />}>
<ClientCard dataPromise={dataPromise} />
</Suspense>
);
}This unlocks real streaming: the server sends HTML for the shell of the page immediately, and the data-dependent parts fill in as the Promises resolve. If you're building UIs with things like glassmorphism components or animated cards where the layout needs to be stable before data arrives, this pattern keeps your skeleton and final content pixel-identical.
Worth noting: you can also check out the nextjs-app-router-guide article for a deeper look at how streaming slots and layouts compose — use() is half the story, the routing model is the other half.
Common Patterns and Mistakes
The most common mistake is creating the Promise inside the component that calls use(). Don't do this:
// BAD — new Promise on every render, causes infinite loop
function BadComponent() {
const data = use(fetch('/api/data').then(r => r.json())); // 💥
return <div>{data.title}</div>;
}Every render creates a new Promise. React suspends, triggers a re-render, which creates another new Promise, which causes another suspension. You'll hit the suspension limit and crash. The Promise must be created *outside* the component or memoized with useMemo if it depends on props.
// GOOD — Promise created in parent or cached
function ParentComponent({ id }: { id: string }) {
// useMemo ensures stable reference across renders
const dataPromise = useMemo(() => fetchData(id), [id]);
return (
<Suspense fallback={<Skeleton />}>
<ChildComponent dataPromise={dataPromise} />
</Suspense>
);
}
function ChildComponent({ dataPromise }: { dataPromise: Promise<Data> }) {
const data = use(dataPromise); // ✅ stable Promise
return <div>{data.title}</div>;
}Another pattern worth knowing: caching. If you're using React's cache() function (available in React 19 and Next.js), you can deduplicate fetch calls across multiple components requesting the same data. combine cache() with use() and you get something pretty close to what React Query's deduplication does, without the extra package.
For teams building design-heavy apps — the kind with layered UI elements you'd find in a glassmorphism generator or complex dashboard — this combination is especially useful. Your data layer stays clean and the UI layer stays declarative.
Error Boundaries and Suspense Together
You need both. Suspense handles the loading state. ErrorBoundary handles the failure state. Neither works without the other in production.
import { Suspense } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
function ErrorFallback({ error }: { error: Error }) {
return (
<div role="alert">
<p>Something went wrong: {error.message}</p>
</div>
);
}
export default function Page() {
const dataPromise = fetchData();
return (
<ErrorBoundary FallbackComponent={ErrorFallback}>
<Suspense fallback={<p>Loading...</p>}>
<DataComponent dataPromise={dataPromise} />
</Suspense>
</ErrorBoundary>
);
}Look, this is one of those things that feels like boilerplate but actually isn't. The ErrorBoundary and Suspense don't have to nest in the same order every time. You can have an outer ErrorBoundary that catches everything, and multiple nested Suspense boundaries for granular loading states. Each data-dependent section of your UI gets its own fallback, and they resolve independently.
React 19's error handling also improved compared to React 18 — uncaught errors in async work now route to the nearest ErrorBoundary correctly, rather than sometimes crashing the entire tree silently. That was a real pain point before 2024.
If you're already using the patterns from react-hooks-complete-guide, adding use() is a small step. The mental model shift is accepting that "loading state" belongs to the component tree structure, not inside individual components via useState.
Should You Use use() Today?
If you're on React 19 and Next.js 15+, yes. The API is stable, the TypeScript types are included, and the streaming benefits are real. If you're on React 18, you don't have access to use() — Suspense for data fetching in React 18 was technically experimental and the behavior changed in React 19.
For most apps, the migration path is: identify components that do their own data fetching with useEffect + useState, convert them to receive a Promise prop, wrap them in Suspense, and call use() on the Promise. You'll write less code. The loading states will be more predictable.
That said, if you're using a dedicated data-fetching library like TanStack Query, use() doesn't replace it — those libraries add caching, background refetching, invalidation, and stale-while-revalidate semantics that use() doesn't touch. They're solving different problems. use() is a low-level primitive; TanStack Query is a full data synchronization layer.
One more thing — React Server Actions combined with use() and the new form actions in React 19 create a genuinely compelling full-stack data flow. The mental model of "this is a function running on the server, its return value is a Promise, the client reads it with use()" is simple enough that it actually sticks. Whether you're building a quick landing page or a complex template (check out our templates collection for inspiration), this pattern scales.
Quick Reference: use() vs. Other Data Patterns
Here's a side-by-side comparison so you don't have to piece it together from memory:
Pattern | Where it runs | Async model | Suspense needed?
---------------------|-------------------|-------------------|-----------------
await (async/await) | Server Components | Native async | No
useEffect + useState | Client Components | Imperative | No (manual)
use(promise) | Client Components | Declarative | Yes
TanStack Query | Client Components | Cache-driven | Optional
SWR | Client Components | Cache-driven | OptionalThe sweet spot for use() is components that receive Promises from their parents, particularly when those Promises come from Server Components. It's not trying to be a cache, a refetch mechanism, or a background sync tool. It's a clean way to read a Promise inside JSX with minimal friction.
If your current codebase has a lot of loading state variables and useEffect fetches, use() won't magically fix them all. But for new components — especially in Next.js App Router — starting with the Suspense + use() pattern from day one results in noticeably cleaner code. Worth the learning curve.
FAQ
Yes — unlike other hooks, use() can be called inside if statements and loops. This is a deliberate design decision that makes it different from every other hook in React.
Not exactly. use() handles reading a Promise declaratively, but you still need to create and manage that Promise somewhere. It replaces the loading/error useState pattern, not the fetch call itself.
No. use() is a React 19 API. React 18 had experimental Suspense for data fetching, but the behavior changed and stabilized in React 19.
React throws the error, and the nearest ErrorBoundary catches it. Without an ErrorBoundary in the tree, you'll get an unhandled error. Always pair Suspense with ErrorBoundary.
