Skip to content
C Codeloom
Next.js

Next.js Data Fetching Patterns: A Practical Guide

Compare the main Next.js data fetching strategies and learn when to use server components, route handlers, SWR, or static generation in your apps.

·5 min read · By Codeloom
Intermediate 9 min read

What you'll learn

  • When to fetch on the server vs the client
  • How Server Components change the model
  • Caching and revalidation strategies
  • Choosing between SSG, ISR, and SSR
  • Using SWR for client interactivity

Prerequisites

  • Familiar with HTTP and JS
  • Basic React knowledge

What and Why

Next.js gives you several places to fetch data: at build time, on each request on the server, on the client after hydration, or inside React Server Components. Each option trades freshness for speed and cost. Picking the right one is the single biggest performance lever you have in a Next.js app, and it directly affects SEO, Time to First Byte, and how much you pay your hosting provider.

The good news is the underlying mental model is small. Once you internalize it, picking the right pattern for a new screen becomes almost mechanical.

Mental Model

Think of a Next.js page as a pipeline with three stages: build, request, and client. Data can enter at any stage, but each stage has different latency, freshness, and cost characteristics.

  • Build stage: cheapest at runtime, stalest data. Good for marketing pages and docs.
  • Request stage: runs per user, sees cookies and headers. Good for personalized HTML.
  • Client stage: runs in the browser, can react to user input. Good for interactive widgets.

Server Components blur the line between the request stage and components. Instead of one big getServerSideProps for the whole page, each component can fetch its own data, and Next.js will dedupe and parallelize for you.

Hands-on Example

Let us build a product page with three regions: a static hero, a server-rendered price block, and a client-side reviews widget.

Build time      Request time         Client time
+---------+     +-------------+      +-------------+
| Hero    | --> | Price block | -->  | Reviews     |
| (SSG)   |     | (RSC fetch) |      | (SWR)       |
+---------+     +-------------+      +-------------+
   |                |                    |
   v                v                    v
cached HTML    fresh per req         hydrates later
Data fetching pipeline across the three stages

The hero rarely changes, so we statically generate it. The price block must be accurate, so we fetch on every request in a Server Component. Reviews update frequently and benefit from client-side polling.

// app/products/[id]/page.tsx (Server Component)
export const revalidate = 3600; // ISR for the static parts

async function getProduct(id: string) {
  const res = await fetch(`https://api.example.com/p/${id}`, {
    next: { revalidate: 60 }, // price refreshes every minute
  });
  return res.json();
}

export default async function Page({ params }: { params: { id: string } }) {
  const product = await getProduct(params.id);
  return (
    <>
      <Hero product={product} />
      <PriceBlock price={product.price} />
      <Reviews productId={product.id} />
    </>
  );
}
// app/products/[id]/Reviews.tsx
"use client";
import useSWR from "swr";

const fetcher = (u: string) => fetch(u).then((r) => r.json());

export function Reviews({ productId }: { productId: string }) {
  const { data } = useSWR(`/api/reviews?id=${productId}`, fetcher, {
    refreshInterval: 30_000,
  });
  if (!data) return <p>Loading reviews...</p>;
  return <ul>{data.map((r: any) => <li key={r.id}>{r.text}</li>)}</ul>;
}

The page above mixes three caching strategies in a single route. Next.js handles the orchestration for you.

Common Pitfalls

  • Fetching the same resource in multiple components without relying on the built-in fetch dedup. Use the same URL and options so Next.js can collapse the requests.
  • Forgetting that fetch in Server Components is cached by default. If you need fresh data, opt out with { cache: 'no-store' }.
  • Mixing getServerSideProps and the App Router in the same project. They follow different rules and confuse new contributors.
  • Sending secrets to the client. A Server Component can read environment variables freely, but anything passed as props to a Client Component crosses the network boundary.
  • Putting heavy data fetching inside a Client Component when a Server Component would do. You pay double: render on the server with no data, then refetch in the browser.

Practical Tips

  • Default to Server Components. Drop into a Client Component only for interactivity.
  • Use route segment config (revalidate, dynamic) explicitly. It makes intent visible during code review.
  • Co-locate data fetching with the component that needs it. This keeps refactors local.
  • For mutations, prefer Server Actions over manually wired API routes. They eliminate one network hop and reduce boilerplate.
  • Use loading.tsx and error.tsx boundaries to keep the user experience smooth while data streams in.
  • Profile with the Next.js build output. It tells you which routes are static, dynamic, or ISR.

Wrap-up

Next.js data fetching is not one feature but a toolbox. Static generation gives you speed, request-time fetching gives you freshness, and client fetching gives you interactivity. Server Components let you mix all three on a single page without paying the framework tax. Start by picking the slowest stage that gives you acceptable freshness, then escalate only when you must. Your users will get faster pages, and your infrastructure bill will thank you.