Technology · Next.js

Next.js Data Fetching

Fetch data in Next.js using server components, static generation, ISR, and client fetching.

TL;DR
  1. 01Fetch data directly in server components with async/await.
  2. 02Use revalidate for static generation with periodic updates.
  3. 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.

Next.js Error HandlingNext.js File Structure