Fetch data in Next.js using server components, static generation, ISR, and client fetching.
export default async function Page() {
const res = await fetch('https://api.example.com/data');
const data = await res.json();
return <div>{data.title}</div>;
}export default async function Products() {
const products = await db.query("SELECT * FROM products");
return <ul>{products.map(p => <li key={p.id}>{p.name}</li>)}</ul>;
}export default async function Dashboard() {
const [user, orders] = await Promise.all([getUser(), getOrders()]);
return <div><UserCard user={user} /><OrderList orders={orders} /></div>;
}// Both Header and Sidebar call getUser() — fetched only once
async function Header() { const u = await getUser(); return <p>{u.name}</p>; }
async function Sidebar() { const u = await getUser(); return <p>{u.role}</p>; }// app/blog/error.tsx catches errors thrown during fetching
export default function Error({ reset }) {
return <button onClick={reset}>Retry</button>;
}// Next.js 15 default: no caching — always fetches fresh data
const res = await fetch('https://api.example.com/data');
// Opt in to caching: revalidate at most every hour
const res = await fetch('https://api.example.com/data', {
next: { revalidate: 3600 }
});const res = await fetch('https://api.example.com/static', {
cache: 'force-cache' // Cached until revalidatePath/revalidateTag is called
});export const revalidate = 86400; // Re-render at most every 24 hours
export default async function Page() {
const data = await fetchStaticData();
return <div>{data.content}</div>;
}const res = await fetch("https://api/posts", {
next: { revalidate: 3600, tags: ["posts"] }
});
// Call revalidateTag("posts") to invalidate all fetches tagged "posts"import { unstable_cache } from "next/cache";
const getCachedUser = unstable_cache(
async (id) => db.user.findUnique({ where: { id } }),
["user"],
{ revalidate: 3600 }
);export default async function Dashboard() {
// Both requests fire at the same time
const [user, orders] = await Promise.all([
fetch("/api/user").then(r => r.json()),
fetch("/api/orders").then(r => r.json())
]);
return <div><UserCard user={user} /><OrderList orders={orders} /></div>;
}const user = await getUser(userId);
const posts = await getPostsByAuthor(user.id); // depends on user.idimport { cache } from "react";
export const getUser = cache(async (id: string) =>
fetch(`/api/users/${id}`).then(r => r.json())
);
// Header and Sidebar both call getUser(id) — only one HTTP request is madeconst results = await Promise.allSettled([
getUser(id),
getRecommendations(id)
]);
const user = results[0].status === "fulfilled" ? results[0].value : null;import { Suspense } from "react";
export default function Page() {
return (
<div>
<FastHeader />
<Suspense fallback=<p>Loading...</p>>
<SlowRecommendations />
</Suspense>
</div>
);
}export async function generateStaticParams() {
const posts = await getPosts();
return posts.map(p => ({ slug: p.slug }));
}
export const revalidate = 86400; // 1 day
export default async function Post({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params; // params is a Promise in Next.js 15
const post = await getPost(slug);
return <article>{post.content}</article>;
}export const dynamicParams = true; // default: render on-demand for new params
// Set to false to return 404 for params not in generateStaticParamsexport const dynamic = "force-static"; // pre-render everything at buildexport const revalidate = 3600;
export async function generateStaticParams() {
const featured = await getFeaturedPosts();
return featured.map(p => ({ slug: p.slug }));
// Other slugs generate on first request, then are cached
}const { slug } = await params;
const post = await getPost(slug);
if (!post) notFound(); // Returns 404 instead of a broken page"use client";
import { useEffect, useState } from "react";
export default function Component() {
const [data, setData] = useState(null);
useEffect(() => {
fetch('/api/data')
.then(r => r.json())
.then(setData);
}, []);
return <div>{data}</div>;
}"use client";
import useSWR from "swr";
const fetcher = (url: string) => fetch(url).then(r => r.json());
export function Profile({ id }) {
const { data, error, isLoading } = useSWR(`/api/users/${id}`, fetcher);
if (isLoading) return <p>Loading...</p>;
if (error) return <p>Error loading profile</p>;
return <p>{data.name}</p>;
}"use client";
import { useQuery } from "@tanstack/react-query";
export function Posts() {
const { data } = useQuery({ queryKey: ["posts"], queryFn: fetchPosts });
return <ul>{data?.map(p => <li key={p.id}>{p.title}</li>)}</ul>;
}// Server component: no loading state, no useEffect needed
export default async function PublicFeed() {
const posts = await getPosts();
return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>;
}Mark the component async and use await with fetch directly in the component body — no useEffect or API routes needed. Next.js automatically deduplicates identical fetch calls within the same render cycle.
Static generation builds pages once at deploy time, while ISR (Incremental Static Regeneration) rebuilds individual pages in the background after a set revalidation interval without triggering a full rebuild. Use ISR when content changes periodically but doesn't need to be real-time.
Pass next: { revalidate: 60 } as a fetch option to regenerate the page at most every 60 seconds, or export const revalidate = 60 at the top of a page/layout file to apply it globally to that route.
Yes — export generateStaticParams to pre-render a subset of dynamic routes at build time, and any unvisited params will be generated on first request and then cached according to your revalidate setting.
Use client-side fetching (SWR or React Query) for data that changes based on user interaction, browser state, or requires real-time updates after the initial page load. Anything that can be fetched at request time without user context belongs in a Server Component.