This content originally appeared on DEV Community and was authored by Ali Aslam
When React 19 introduced the use
hook, it quietly changed one of the oldest patterns in React — the way we deal with async data.
No more scattering your fetch logic in useEffect
and juggling loading states manually.
No more “render first, then fetch” when you really want “fetch first, then render.”
In this deep dive, we’ll unpack how use
works, why it feels different from anything you’ve used before, and how it fits perfectly with Suspense to make async rendering feel natural.
Before we jump in, here’s your roadmap.
Table of Contents
-
Why the
use
Hook Exists- Enter the
use
Hook - Side-by-Side: Old vs New
- How It Actually Works
- Enter the
-
Using
use
in Server vs Client Components- In Server Components — The Happy Path
- In Client Components — The Experimental Side
- Quick Visual — Where You Can Use
use
Today - Key Takeaway
-
How It Works With Suspense
- The Flow
- Code Example
- What’s Different From
useEffect
- Diagram — Data Flow
- Error Handling
-
Real-World Patterns
- Reading From a Shared Async Cache
- Integrating With Frameworks (Nextjs Example)
- Combining With Client-Side State
- Async Local Resources
- SSR + Hydration Flow
-
Common Pitfalls
- Forgetting to Wrap in
<Suspense>
- Using It in Client Components Without Experimental Build
- Over-Fetching on Every Render
- Expecting
use
to Cancel Work - Mixing With
useEffect
for the Same Data - Forgetting About Error Boundaries
- Forgetting to Wrap in
-
Wrap-Up
- When to Reach for
use
- The Big Mental Model
- When to Reach for
Why the use
Hook Exists
Before React 19, fetching data directly inside a component was… a little awkward.
You couldn’t just “await” something in your JSX.
If you tried, you’d get syntax errors, strange infinite loops, or React yelling at you about hooks.
So, what did we do instead?
We reached for the trusty useEffect
+ state pattern:
function Products() {
const [products, setProducts] = useState([]);
useEffect(() => {
fetch("/api/products")
.then((res) => res.json())
.then(setProducts);
}, []);
return <ProductList products={products} />;
}
It worked, but it had downsides:
- Extra render — First render with empty state, then render again with data.
-
Data fetching spread out — Logic is split between
useEffect
and render. -
Not great for SSR —
useEffect
doesn’t run until after hydration, so the server render can’t preload the data this way.
Enter the use
hook
React 19 introduces a new primitive: use(promise)
.
It’s a hook that lets you directly “await” a promise during render — no useEffect
, no splitting logic, no extra render.
function Products() {
const products = use(fetch("/api/products").then(res => res.json()));
return <ProductList products={products} />;
}
Boom.
One render, data already in place.
When you wrap this in a <Suspense>
boundary, React will pause rendering until the data is ready, and show your loading UI in the meantime.
Side-by-side: Old vs New
Old way (useEffect ) |
New way (use ) |
---|---|
Render once with no data | Suspends until data ready |
Split logic across hooks & JSX | All logic inline in JSX |
Not SSR-friendly | Perfect for Server Components |
How it actually works
Here’s the mental model:
Component tries to use a promise → `use()` "throws" it → React catches it → Suspense fallback shows → Promise resolves → React continues render with the result
In other words, use()
tells React:
“Hey, I need this async thing before I can finish rendering.
Hold my JSX until it’s done.”
Here’s a simple diagram:
┌────────────┐
│ use(promise)───► Promise not resolved? Throw!
└────────────┘
│
▼
<Suspense> fallback renders
│
Promise resolves
▼
Component finishes rendering with value
Using use
in Server vs Client Components
The use
hook is fully stable in Server Components — and only experimental in Client Components (for now).
That means how and where you use it depends on your component type.
1. In Server Components — The Happy Path
In a Server Component, use
just works.
You can throw any promise at it — a fetch
call, a database query, or even a custom async function — and React will wait for it before sending HTML to the browser.
Example:
// Server Component
export default function ProductPage({ id }) {
const product = use(getProduct(id));
return <ProductDetails product={product} />;
}
async function getProduct(id) {
const res = await fetch(`https://api.example.com/products/${id}`);
return res.json();
}
Why this is so nice:
- No
useEffect
or client-side fetching required. - The user gets fully rendered HTML on first load.
- Perfect for SEO and performance.
2. In Client Components — The Experimental Side
Right now, using use
inside a Client Component is still marked as experimental.
You’ll need to opt into the experimental build of React if you want to try it.
Example:
"use client";
import { use } from "react";
export default function Notes() {
const notes = use(getNotesFromIndexedDB());
return <NotesList notes={notes} />;
}
This can be powerful — it lets you read from async local resources (like IndexedDB or caches
) during render.
But because it’s still experimental, you’ll hit limitations:
- Only works in experimental React builds.
- May change before it’s stable.
- Requires wrapping in
<Suspense>
for a loading state.
3. Quick Visual — Where You Can Use use
Today
✅ Server Components → Fully stable in React 19
⚠ Client Components → Experimental, not for production unless you accept risk
4. Key takeaway
If you’re in a React 19 Server Component — use
is your new best friend.
If you’re in a Client Component, think of it as a sneak preview of the future.
How It Works With Suspense
At first glance, use
feels like it’s doing magic:
You just pass it a promise, and somehow your component waits for that promise before rendering.
But under the hood, the magic is actually Suspense doing its thing.
The Flow
- Your component calls
use(promise)
. - If the promise is still pending,
use
throws it. - React catches that promise and says:
“Alright, let’s pause here and render the nearest
<Suspense>
fallback instead.”
- Once the promise resolves, React re-renders the component — now with the resolved value.
Code Example
export default function Page() {
const product = use(fetchProduct());
return (
<ProductDetails product={product} />
);
}
async function fetchProduct() {
const res = await fetch("https://api.example.com/products/42");
return res.json();
}
Wrapped in Suspense:
<Suspense fallback={<p>Loading product...</p>}>
<Page />
</Suspense>
What’s Different From useEffect
-
useEffect
: Runs after the initial render → shows empty UI first, then fetches, then re-renders. -
use
+ Suspense: Pauses the initial render until data is ready → shows fallback while waiting, then renders final UI once.
Diagram — Data Flow
Render starts
↓
use(promise) called
↓
Promise still pending → throw it
↓
Nearest <Suspense> shows fallback
↓
Promise resolves
↓
Component resumes render with resolved value
Error Handling
If the promise rejects, the error gets sent to the nearest Error Boundary instead of a <Suspense>
fallback.
<ErrorBoundary fallback={<p>Could not load product.</p>}>
<Suspense fallback={<p>Loading...</p>}>
<Page />
</Suspense>
</ErrorBoundary>
Key point:
use
doesn’t load the data for you — it just turns any promise into a Suspense trigger so React can coordinate loading states and rendering.
Real-World Patterns
Once you understand use
+ Suspense, you start seeing all the possibilities.
It’s not just for “fetch some data from an API” — it can be used with any promise.
Let’s go through some patterns you’ll actually use in real apps.
1. Reading From a Shared Async Cache
If you have a resource that multiple components need, you can put it in a simple cache so the same promise is reused — and use
only waits once.
// cache.js
const productCache = new Map();
export function getProduct(id) {
if (!productCache.has(id)) {
const promise = fetch(`/api/products/${id}`).then(res => res.json());
productCache.set(id, promise);
}
return productCache.get(id);
}
// Server Component
export default function Product({ id }) {
const product = use(getProduct(id));
return <h1>{product.name}</h1>;
}
Benefit:
If multiple components render the same product, they share the promise — no duplicate fetches.
2. Integrating With Frameworks (Next.js Example)
In frameworks like Next.js 13+, use
is perfect for Server Components that need to fetch data.
export default function Page({ params }) {
const post = use(getPost(params.slug));
return <BlogPost post={post} />;
}
This blends perfectly with Next.js’s automatic Suspense handling.
3. Combining With Client-Side State
Sometimes you fetch initial data with use
in a Server Component, then update it on the client.
"use client";
import { useState } from "react";
export default function Comments({ initialComments }) {
const [comments, setComments] = useState(initialComments);
function addComment(newComment) {
setComments([...comments, newComment]);
}
return (
<>
<CommentList comments={comments} />
<CommentForm onSubmit={addComment} />
</>
);
}
Here, the initial comments load instantly (SSR), but user interactions happen client-side.
4. Async Local Resources
use
can also work with async things inside the browser, like IndexedDB.
"use client";
import { use } from "react";
export default function Settings() {
const settings = use(loadSettingsFromIndexedDB());
return <SettingsForm settings={settings} />;
}
Just remember: still experimental in client components.
5. SSR + Hydration Flow
With use
in Server Components:
- Data fetched on the server.
- HTML sent to the client already containing the data.
- No loading spinner on first render (unless it’s client-side only).
This means faster first paint and better SEO.
In short:
If it returns a promise, use
can help you render with it synchronously from the perspective of your JSX, without juggling state and effects.
Common Pitfalls
Like any new feature, use
can trip you up if you don’t know its limits.
Let’s go through the mistakes I see most often — so you can dodge them.
1. Forgetting to Wrap in <Suspense>
If you call use
with a pending promise but don’t have a <Suspense>
above your component, React won’t know what fallback UI to show.
You’ll either get an error or nothing will render until the data arrives.
Fix: Always wrap use
calls in a <Suspense>
with a clear fallback
.
<Suspense fallback={<p>Loading...</p>}>
<Product />
</Suspense>
2. Using It in Client Components Without Experimental Build
On the client side, use
is still experimental in React 19.
If you try it in a normal production build, you’ll hit errors.
Fix:
- Use it in Server Components for stable production use.
- Only use it in Client Components if you’ve opted into the experimental React build.
3. Over-Fetching on Every Render
If your promise-creating function isn’t stable, use
will get a fresh promise every render — meaning you’ll refetch constantly.
Bad:
const product = use(fetch(`/api/products/${id}`).then(res => res.json()));
This runs a new fetch
each render.
Good:
const product = use(getProduct(id)); // getProduct caches the promise
4. Expecting use
to Cancel Work
use
will pause rendering until the promise resolves — but if the user navigates away, React doesn’t cancel the fetch for you.
You’ll need AbortController or your data library to handle that.
5. Mixing With useEffect
for the Same Data
Don’t fetch the same data twice — once with use
and once in useEffect
.
Pick one pattern for that data source, or you’ll waste resources.
6. Forgetting About Error Boundaries
If the promise rejects, and you don’t have an Error Boundary, the whole render will blow up.
Suspense handles loading states, not errors.
<ErrorBoundary fallback={<p>Failed to load data.</p>}>
<Suspense fallback={<p>Loading...</p>}>
<Product />
</Suspense>
</ErrorBoundary>
In short:
Use use
confidently, but remember it’s not a magic “fetch everything safely” button — you still need caching, fallbacks, and error handling.
Wrap-Up
The use
hook in React 19 is one of those features that makes you think:
“Wait… why wasn’t this always possible?”
It takes one of the most common needs in React — using async data — and lets you do it right in the render phase.
No more juggling useEffect
and useState
just to get some JSON on the screen.
No more splitting your logic into “render part” and “fetch part.”
Instead, with use
+ Suspense:
- You write code that reads top-to-bottom, just like synchronous code.
- Your UI pauses exactly where it needs to, and React fills in the gaps with loading states.
- Data fetching in Server Components becomes a first-class citizen — no hacks required.
When to Reach for use
Server Components? All day, every day.
Client Components? Only if you’re on experimental React and know the risks.
Data that’s cacheable or shared across renders.
Not for cases where you need incremental loading inside a single component without Suspense.
The Big Mental Model
Think of use
as React’s way of saying:
“If you give me a promise, I’ll handle the waiting.
You just tell me what the UI should look like before and after.”
And when your teammates ask, “Wait, how is this async fetch happening **inside* the render?”*
You can just smile and say:
“It’s a Suspense thing — and now it’s built right into React.”
Up next: React 19 Suspense Deep Dive — Data Fetching, Streaming, and Error Handling Like a Pro
Follow me on DEV for future posts in this deep-dive series.
https://dev.to/a1guy
If it helped, leave a reaction (heart / bookmark) — it keeps me motivated to create more content
Want video demos? Subscribe on YouTube: @LearnAwesome
This content originally appeared on DEV Community and was authored by Ali Aslam