Technology · Next.js
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.
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>; }
Static Generation with Caching
- Cache fetches for static generation.
export default async function Page() { const res = await fetch('https://api.example.com/data', { next: { revalidate: 3600 } // Cache for 1 hour }); const data = await res.json(); return <div>{data}</div>; } - fetch requests are cached by default.
// This is cached indefinitely const res = await fetch('https://api.example.com/data'); // Never cache (always fresh) const res = await fetch('https://api.example.com/data', { cache: 'no-store' }); - Use force-cache explicitly to opt into long-lived caching.
const res = await fetch('https://api.example.com/static', { cache: 'force-cache' // Cached until manually revalidated }); - Set revalidate at the route segment level to cache 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 unstable_cache to cache 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 } );
ISR and Revalidation
- Regenerate static pages periodically in the background.
export const revalidate = 3600; // Revalidate every hour export default async function Page() { const data = await fetchData(); return <article>{data.content}</article>; } - Pages are served statically until revalidation time expires.
- Next request triggers a background rebuild.
- Use revalidatePath to trigger on-demand revalidation after a mutation.
"use server"; import { revalidatePath } from "next/cache"; export async function updatePost(id: string, data) { await db.post.update({ where: { id }, data }); revalidatePath("/blog"); } - Use revalidateTag to invalidate grouped cached fetches.
const res = await fetch("https://api/posts", { next: { tags: ["posts"] } }); // Later: import { revalidateTag } from "next/cache"; revalidateTag("posts"); // invalidates all fetches tagged "posts" - Set revalidate = 0 to disable caching and always fetch fresh data.
export const revalidate = 0; // Opt into dynamic rendering
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 }) { const post = await getPost(params.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 post = await getPost(params.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.
"use client"; import useSWR from "swr"; export function Profile({ id }) { const { data, error, isLoading } = useSWR(`/api/users/${id}`, fetch); 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>; }
Tip: Fetch data in server components whenever possible — it's faster and more secure than client-side fetching.
Warning: Don't fetch the same data multiple times — use fetch caching to reuse responses automatically.