Next.js Data Fetching
Fetch data in Next.js using server components, static generation, ISR, and client fetching.
TL;DR
- 01Fetch data directly in server components with async/await.
- 02Use revalidate for static generation with periodic updates.
- 03ISR updates pages in background without full rebuild.
Tips
- 01Fetch data in server components whenever possible — it's faster and more secure than client-side fetching.
Warnings
- 01Don't fetch the same data multiple times — use fetch caching to reuse responses automatically.
Server Component Data Fetching
- Fetch data directly in async server components.
export default async function Page() { const res = await fetch('https://api.example.com/data'); const data = await res.json(); return <div>{data.title}</div>; } - Data is fetched on the server, only HTML is sent to browser.
- Can access databases and secrets securely.
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>; } - Fetch multiple resources in parallel to reduce total wait time.
export default async function Dashboard() { const [user, orders] = await Promise.all([getUser(), getOrders()]); return <div><UserCard user={user} /><OrderList orders={orders} /></div>; } - Next.js deduplicates identical fetch calls within the same request.
// 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>; } - Use error boundaries to handle failed fetch requests gracefully.
// app/blog/error.tsx catches errors thrown during fetching export default function Error({ reset }) { return <button onClick={reset}>Retry</button>; }
Fetch Cache Configuration
- In Next.js 15, fetch is uncached by default — opt in to caching explicitly.
// 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 } }); - Use force-cache for data that should be cached indefinitely until manually invalidated.
const res = await fetch('https://api.example.com/static', { cache: 'force-cache' // Cached until revalidatePath/revalidateTag is called }); - Set revalidate at the route segment level to apply caching to the whole page.
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>; } - Use next:{tags:[...]} to group cached fetches for tag-based invalidation.
const res = await fetch("https://api/posts", { next: { revalidate: 3600, tags: ["posts"] } }); // Call revalidateTag("posts") to invalidate all fetches tagged "posts" - Use unstable_cache to apply caching to non-fetch data like database queries.
import { unstable_cache } from "next/cache"; const getCachedUser = unstable_cache( async (id) => db.user.findUnique({ where: { id } }), ["user"], { revalidate: 3600 } );
Parallel and Sequential Fetching
- Fetch multiple independent resources in parallel to avoid waterfall delays.
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>; } - Use sequential awaits only when later data depends on earlier results.
const user = await getUser(userId); const posts = await getPostsByAuthor(user.id); // depends on user.id - Wrap repeated data calls with React cache() so multiple components share one request.
import { 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 made - Use Promise.allSettled to fetch multiple resources without short-circuiting on error.
const results = await Promise.allSettled([ getUser(id), getRecommendations(id) ]); const user = results[0].status === "fulfilled" ? results[0].value : null; - Suspend individual slow sections with Suspense so fast data renders first.
import { Suspense } from "react"; export default function Page() { return ( <div> <FastHeader /> <Suspense fallback=<p>Loading...</p>> <SlowRecommendations /> </Suspense> </div> ); }
Dynamic Parameters with ISR
- Generate pages for multiple parameters statically.
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>; } - Pre-renders all post pages at build time.
- Use dynamicParams to control behavior for unknown params.
export const dynamicParams = true; // default: render on-demand for new params // Set to false to return 404 for params not in generateStaticParams - Pass fallback data while a new page generates for the first time.
export const dynamic = "force-static"; // pre-render everything at build - Combine generateStaticParams with revalidation for hybrid pages.
export 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 } - Use notFound() inside the page to handle deleted resources gracefully.
const { slug } = await params; const post = await getPost(slug); if (!post) notFound(); // Returns 404 instead of a broken page
Client-Side Data Fetching
- Fetch data on client with useEffect.
"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 for real-time data or user-specific content.
- Use SWR for built-in caching, revalidation, and error states — pass a fetcher that returns parsed JSON.
"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 React Query for more advanced caching and mutation workflows.
"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>; } - Prefer server components over client fetching when data isn't user-specific.
// 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>; }
FAQ
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.