React 19 + Next.js 15: use() for Data Fetching
🔍 What Is use() and Why Should You Care?
React 19 introduces the use() hook, a new primitive that allows components to directly suspend until a promise resolves.
This is particularly useful in frameworks like Next.js 15, which has built-in support for the use() hook on the server, enabling simpler, cleaner, and more readable data fetching.
Traditional Data Fetching (React 18)
Before:
// React 18
useEffect(() => {
fetchData().then(setData);
}, []);
Now with use():
// React 19 + Next.js 15
const data = use(fetchData());
This allows synchronous-style code for asynchronous logic.
⚙️ Where Can You Use use()?
| Location | Allowed to use use()? | Notes |
|---|---|---|
| Server Components | ✅ Yes | Best place to use it |
| Client Components | ❌ No | Not supported (throws error) |
app/ directory (Next) | ✅ Yes (Server only) | Great for SSR/SSG |
use()is server-only (for now). It pairs naturally with Server Components and the Next.jsapp/directory structure.
✅ Best Practices for Using use() in Next.js 15
1) Colocate Your Data Fetching
In the past, we separated data fetching from UI logic. With use(), colocating logic makes code easier to maintain.
// app/products/page.tsx
import { use } from "react";
import { getProducts } from "@/lib/api";
export default function Page() {
const products = use(getProducts());
return <pre>{JSON.stringify(products, null, 2)}</pre>;
}
2) Use Data Fetching Helpers in /lib or /data
Abstract your API logic:
// lib/api.ts
export async function getProducts() {
const res = await fetch("https://api.example.com/products");
if (!res.ok) throw new Error("Failed to fetch products");
return res.json();
}
This separation keeps UI code clean and logic reusable.
3) Combine with cache() for Performance
React’s cache() utility helps memoize fetches across requests:
import { cache } from "react";
export const getProducts = cache(async () => {
const res = await fetch("https://api.example.com/products");
if (!res.ok) throw new Error("Failed to fetch products");
return res.json();
});
This avoids repeated API calls on the server when rendering multiple components that need the same data.
4) Handle Errors Gracefully with Error Boundaries
Because use() suspends for data and throws on errors, pair it with Next.js’s built-in error boundaries:
// app/products/error.tsx
"use client";
export default function Error({ error }: { error: Error }) {
return <p>Error loading products: {error.message}</p>;
}
5) Use Suspense for Progressive Loading
use() suspends components until data is ready. Combine it with <Suspense> for fallbacks:
// app/products/page.tsx
import { Suspense } from "react";
import ProductList from "./ProductList";
export default function Page() {
return (
<Suspense fallback={<p>Loading products...</p>}>
<ProductList />
</Suspense>
);
}
🧱 Suggested Project Structure
/app
/products
page.tsx → Server Component using use()
error.tsx → Error boundary
/lib
api.ts → Cached fetchers
This structure maximizes modularity and takes full advantage of React’s new data-fetching capabilities.
🧩 When Not to Use use()
- Client Components: You can’t use
use()here. UseuseEffect()or libraries like SWR or React Query. - Highly Dynamic Interactions: If you need to fetch based on client input (e.g., search),
use()isn’t appropriate. - Auth or Browser APIs: These need to run client-side.
📌 Summary
| Feature | use() in Next.js 15 + React 19 |
|---|---|
| Best Location | Server Components in /app |
| Benefits | Cleaner code, colocation, better SSR |
| Combine With | cache(), Suspense, error.tsx |
| Avoid In | Client Components |
🔮 Final Thoughts
The use() hook represents a major shift toward declarative, colocated, and simplified data fetching. With full support in Next.js 15 and React 19, developers can finally write server-side data logic in a way that feels like plain React.
Start adopting this pattern today and simplify your codebase — both in readability and performance.
See you in the next article!