React 19 + Next.js 15: use() for Data Fetching


Back

🔍 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()?

LocationAllowed to use use()?Notes
Server Components✅ YesBest place to use it
Client Components❌ NoNot 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.js app/ 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. Use useEffect() 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

Featureuse() in Next.js 15 + React 19
Best LocationServer Components in /app
BenefitsCleaner code, colocation, better SSR
Combine Withcache(), Suspense, error.tsx
Avoid InClient 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!