background-shape
Next.js 12 Data Fetching, SSG, SSR, and ISR Explained
April 20, 2022 · 6 min read · by Muhammad Amal programming

TL;DRgetStaticProps (SSG) for content known at build time. Plus ISR with revalidate for content that changes occasionally. getServerSideProps (SSR) for per-request data, auth-gated pages, or anything that can’t be cached. Client-side fetching (SWR/React Query) for in-app interactions. Pick the slowest strategy that meets your freshness needs.

After routing, the next Next.js concept is data fetching. There are three server-side strategies (SSG, SSR, ISR) plus client-side fetching for in-app data. Picking the right one per page is the difference between a fast site and a slow one.

This post walks through each strategy with the cases where each one wins, and the gotchas that catch teams new to Next.js.

getStaticProps (SSG) — build-time data

getStaticProps runs at build time. The page renders to HTML once and serves as static files from then on. Fastest possible page; oldest possible data.

// pages/blog/[slug].tsx
import type { GetStaticProps, GetStaticPaths } from 'next';

interface Post {
  slug: string;
  title: string;
  body: string;
}

interface Props { post: Post }

export default function BlogPost({ post }: Props) {
  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.body }} />
    </article>
  );
}

export const getStaticPaths: GetStaticPaths = async () => {
  const posts = await loadAllPosts();
  return {
    paths: posts.map(p => ({ params: { slug: p.slug } })),
    fallback: 'blocking',
  };
};

export const getStaticProps: GetStaticProps<Props> = async ({ params }) => {
  const post = await loadPost(params!.slug as string);
  if (!post) return { notFound: true };
  return { props: { post } };
};

Two functions for dynamic SSG routes:

  • getStaticPaths — which paths exist? Build time enumeration.
  • getStaticProps — for each path, what data? Build time fetch.

fallback: 'blocking' means new paths not in getStaticPaths get server-rendered on first request, then cached. Other options:

  • false — only paths in getStaticPaths exist; everything else 404s
  • true — render placeholder, fetch in background (rare; weird UX)

For most cases, 'blocking' is right.

Use SSG for: marketing pages, blog posts, documentation, product catalogs (with thousands of paths). Anything where “data hasn’t changed in 5 minutes” is acceptable.

getStaticProps + revalidate (ISR) — eventually-fresh static

Add revalidate to getStaticProps’s return:

export const getStaticProps: GetStaticProps<Props> = async ({ params }) => {
  const post = await loadPost(params!.slug as string);
  return {
    props: { post },
    revalidate: 60,    // re-generate at most every 60 seconds
  };
};

Incremental Static Regeneration. Page is static until revalidate seconds pass; next request triggers a background regeneration; subsequent requests get the new version.

The flow per page:

  1. User requests /blog/foo. Cached static HTML served instantly.
  2. If > 60 seconds since last regeneration, Next.js triggers a regen in the background. Current request still gets old HTML.
  3. Regen completes, cache updates.
  4. Next request gets fresh HTML.

So users see stale-while-revalidating data. Fast pages, eventually-consistent.

Use ISR for: product pages with prices that change daily, blog post lists, anything where “5-minute lag is fine but 5-hour lag isn’t.”

For on-demand revalidation (immediately invalidate a specific page on a webhook), Next.js 12.1 added res.revalidate(path) in API routes:

// pages/api/revalidate.ts
export default async function handler(req, res) {
  if (req.headers['x-secret'] !== process.env.REVALIDATE_SECRET) {
    return res.status(401).end();
  }
  await res.revalidate('/blog/' + req.query.slug);
  res.json({ revalidated: true });
}

Webhook from your CMS calls this, page updates immediately.

getServerSideProps (SSR) — per-request data

Runs on every request. Page is rendered fresh each time.

import type { GetServerSideProps } from 'next';

export const getServerSideProps: GetServerSideProps<Props> = async (ctx) => {
  const session = await getSession(ctx.req);
  if (!session) {
    return { redirect: { destination: '/login', permanent: false } };
  }
  const user = await loadUser(session.userId);
  return { props: { user } };
};

export default function Dashboard({ user }) {
  return <h1>Welcome {user.name}</h1>;
}

Use SSR for:

  • Pages that depend on the current user (auth-gated, personalized)
  • Pages whose data changes per-request (live prices, dashboards)
  • Pages that need request context (headers, cookies, IP)

Cost: every request hits your server. No CDN caching of the HTML. Latency = your server’s roundtrip.

Client-side fetching — for in-app interactions

For data that loads after the page loads (e.g., a list that paginates, a chart that updates), don’t use SSR/SSG — use client-side fetching:

import useSWR from 'swr';

function OrderList() {
  const { data, error, isLoading } = useSWR('/api/orders', fetcher);
  if (isLoading) return <Skeleton />;
  if (error) return <Error />;
  return <ul>{data.orders.map(o => <li key={o.id}>{o.id}</li>)}</ul>;
}

SWR or React Query handle caching, revalidation, and loading states. Pair with Suspense if you want declarative loading.

Decision matrix

The cheat sheet I use:

Question Strategy
Same content for all users, changes rarely (docs, marketing) SSG (getStaticProps)
Same for all users, changes occasionally (blog, products) SSG + ISR
Different per user, can’t cache (dashboard, profile) SSR (getServerSideProps)
Loaded after initial page render (in-app interactions) Client (SWR/React Query)
Mix: shell SSG/SSR, body dynamic SSG + client fetching

Default to SSG. Promote to ISR if updates need to land within minutes. Promote to SSR only when per-request data is genuinely required.

A real-world layout

For our blog (the one you’re reading):

Page Strategy
/ (homepage) SSG
/blog/ (post list) SSG + ISR (revalidate 300)
/blog/[slug] (post) SSG + ISR (revalidate 600)
/tracks/[track]/ SSG + ISR
/contact/ SSG

No SSR anywhere. The blog has no per-user content. Every page is statically built, with ISR for the post list (so new posts appear within 5 minutes without rebuild).

For our internal admin dashboard, by contrast:

Page Strategy
/login/ SSG
/dashboard/ SSR (per-user)
/orders/[id]/ SSR (per-user, real-time data)
/orders/ (list with filters) SSR shell + client-side pagination

Different shape of app, different strategy mix.

getServerSideProps gotchas

A few specifics worth knowing:

Don’t fetch from your own API. If page X needs data from /api/data, call your data layer directly inside getServerSideProps, not via fetch to your own API. Avoids a round trip.

Cache headers don’t apply to the HTML. SSR pages are dynamic; CDNs won’t cache them. If your data is mostly-static, use ISR instead.

ctx.req and ctx.res are Node objects. You can read cookies, set response headers, etc. Useful for auth — but easy to leak server-only logic into client bundles.

Errors throw to a 500 page. Wrap in try/catch and use { notFound: true } or { redirect: ... } for non-500 outcomes.

Common Pitfalls

Using SSR by default. It’s the slowest. Default to SSG; promote only when needed.

SSR with no auth check. “Page renders on the server” doesn’t mean “page is secret.” SSR pages are returned to anyone who requests them. Auth-check inside getServerSideProps.

Client-side fetching for above-the-fold data. Causes layout shift and slow LCP. Use SSG/SSR for what’s in the first viewport; client for what’s below.

ISR revalidate: 1. Effectively SSR with extra steps and worse latency. If you need per-request freshness, just use SSR.

Forgetting getStaticPaths.fallback. Without it, dynamic SSG routes 404 for paths not pre-generated. 'blocking' is the safe default.

Defining a page as async function. Pages must be regular React components. Async happens in getStaticProps / getServerSideProps.

Importing server-only code into pages. A page file is bundled for both server and client (the component part). Anything you import is included in both. Server-only deps (database client, secrets) should only be imported inside getStaticProps / getServerSideProps, which are tree-shaken from the client bundle.

Wrapping Up

Pick the slowest data-fetching strategy that meets your freshness requirements. Default to SSG, add ISR when needed, reach for SSR only for genuinely per-request data, use client-side fetching for everything that loads after the shell. Friday: Next.js middleware for auth and redirects — running code before your pages render.