Search across all documentation pages
Fetch data directly inside async Server Components with built-in caching and deduplication.
Quick-reference recipe card -- copy-paste ready.
// app/posts/page.tsx (Server Component -- the default)
export default async function PostsPage() {
// Cached by default (force-cache)
const res = await fetch("https://api.example.com/posts");
const posts: Post[] = await res.json();
return (
<ul>
{posts.map((p) => (
<li key={p.id}>{p.title}</li>
))}
</ul>
);
}
// Time-based revalidation
await fetch(url, { next: { revalidate: 60 } });
// No caching -- always fresh
await fetch(url, { cache: "no-store" });
// Tag-based revalidation
await fetch(url, { next: { tags: ["posts"] } });When to reach for this: You need to load data for a page or component that has no interactivity -- no useState, no onClick, no browser APIs.
// lib/api.ts
export type Post = { id: number; title: string; body: string };
export async function getPosts(): Promise<Post[]> {
const res =
// app/posts/page.tsx
import { getPosts } from "@/lib/api";
import Link from "next/link";
export default async function PostsPage() {
const posts = await getPosts();
return (
<main className
// app/posts/[id]/page.tsx
import { getPost } from "@/lib/api";
import { notFound } from "next/navigation";
type Props = { params: Promise<{ id: string }> };
export default async function PostPage({
What this demonstrates:
useEffect, no client-side loading statelib/ file for reuse across componentsnext.revalidate for time-based ISR and next.tags for on-demand revalidationparams as a Promise (Next.js 15+ pattern)async and call await fetch() directly in the function body.fetch API with cache and next options. The default behavior in Next.js 15+ is cache: "auto", which lets the framework decide based on context.error.tsx boundary.Parallel data fetching (avoid waterfalls):
export default async function DashboardPage() {
// Start both fetches simultaneously
const [users, orders] = await Promise.all([
fetch("https://api.example.com/users").then((r) => r.json()),
Non-fetch data sources (database, ORM):
import { cache } from "react";
import { db } from "@/lib/db";
// Wrap with React.cache for request-level memoization
export const getUser = cache(async (id: string) => {
return db.user.findUnique({ where: { id } });
Passing a promise to a Client Component:
// Server Component
export default async function Page() {
const dataPromise = fetchSlowData(); // do NOT await
return <ClientChart dataPromise={dataPromise} />;
}// Always type your fetch responses
type ApiResponse<T> = { data: T; total: number };
async function getItems(): Promise<ApiResponse<Item[]>> {
const res = await
Fetch waterfalls -- Sequential await calls create waterfalls where each fetch waits for the previous one. Fix: Use Promise.all() for independent requests or move each fetch into its own Suspense-wrapped component for parallel streaming.
Forgetting error handling -- An uncaught fetch error crashes the entire route. Fix: Throw an error from your fetch helper so the nearest error.tsx boundary catches it, or use try/catch for granular handling.
cache: "no-store" makes the entire route dynamic -- If any fetch in a route uses no-store, the whole route opts out of static generation. Fix: Isolate dynamic fetches into separate components wrapped with Suspense so the rest of the route can remain static.
Non-fetch calls are not deduplicated automatically -- Direct database queries or third-party SDK calls bypass React's request memoization. Fix: Wrap them with React.cache() to get per-request deduplication.
params and searchParams are Promises in Next.js 15+ -- Destructuring them directly without await gives you a Promise object, not the values. Fix: Always await params and await searchParams before accessing properties.
| Alternative | Use When | Don't Use When |
|---|---|---|
| Route Handlers (API routes) | You need a standalone REST endpoint for external consumers | You only need data inside a Server Component |
| SWR or React Query on the client | You need real-time updates, polling, or optimistic mutations | Data can be fetched once on the server |
| Server Actions | You need to mutate data, not read it | You only need GET requests |
React.cache + ORM | You fetch from a database, not an HTTP API | You are calling a public REST API |
unstable_cache (Next.js) | You need Data Cache semantics for non-fetch data sources | Plain fetch with next.revalidate already works |
useEffect or libraries like SWR for async data fetching in Client Componentscache: "no-store" and next: { revalidate: 0 }?cache: "no-store" bypasses the Data Cache entirelyrevalidate: 0 also skips caching but signals intent through the ISR APIReact.cache() instead of relying on automatic fetch deduplication?React.cache() for non-fetch data sources like direct database queries or ORM callsfetch APIReact.cache() provides per-request memoization for any async functionparams prop in a dynamic route page component?// Next.js 15+: params is a Promise
type Props = { params: Promise<{ id: string }> };
export default async function Page({ params }: Props) {
const { id } =
params without awaiting it in Next.js 15+?"[object Promise]"await params before accessing properties// Use Promise.all to run fetches in parallel
const [users, orders] = await Promise.all([
fetch("/api/users").then((r) => r.json()),
fetch("/api/orders").
cache: "no-store" on one fetch make the entire route dynamic?<Suspense>type ApiResponse<T> = { data: T; total: number };
async function getItems(): Promise<ApiResponse<Item[]>> {
const res = await
fetch in Next.js 15+?cache: "auto", which lets the framework decide based on contextforce-cachecache or next.revalidate on every fetch call// Server Component: pass the promise, not the resolved value
export default async function Page() {
const dataPromise = fetchSlowData(); // do NOT await
return <ClientChart dataPromise={dataPromise} />;
}use() hook from Reacterror.tsx boundary catches it if one exists