Data Fetching and Rendering Modes

Server Components fetching data directly, static/dynamic/ISR rendering modes, and when to use Client Components.

Server Components fetch data directly

In the App Router, every component is a Server Component by default — it renders on the server, and can be an async function that fetches data directly, with no useEffect, no loading state, and no client-side network waterfall:

Typescript
// app/posts/page.tsx
interface Post {
  id: number;
  title: string;
}

async function getPosts(): Promise<Post[]> {
  const res = await fetch('https://api.example.com/posts');
  return res.json();
}

export default async function PostsPage() {
  const posts = await getPosts();

  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

The fetch call, and the rendering it feeds, both happen on the server before any HTML reaches the browser — the client receives finished markup, not a spinner followed by a second network round-trip.

The rendering modes

Next.js decides, per route, when that rendering actually happens — conceptually there are three modes:

Mode When the HTML is generated Typical use
Static (default) Once, at build time, then reused for every request Marketing pages, docs, blog posts that rarely change
Dynamic Freshly, on every incoming request Pages showing per-user or highly time-sensitive data (a dashboard, a cart)
Incremental Static Regeneration (ISR) At build time, then automatically regenerated in the background after a set interval Content that changes occasionally but doesn't need to be real-time (product listings, articles)

A route becomes dynamic automatically as soon as it does something inherently per-request — reading cookies or headers, or fetching with { cache: 'no-store' }. Otherwise, Next defaults to static rendering. ISR is opted into with a revalidate value:

Typescript
async function getPosts(): Promise<Post[]> {
  const res = await fetch('https://api.example.com/posts', {
    next: { revalidate: 60 }, // regenerate this data at most once every 60 seconds
  });
  return res.json();
}

The first request after 60 seconds still gets the previously cached page instantly, while Next regenerates a fresh version in the background for subsequent requests — visitors are never blocked waiting on regeneration.

Client Components, and when you actually need one

Server Components can't use state, effects, browser-only APIs, or event handlers like onClick — for that you need a Client Component, opted into with a 'use client' directive at the very top of the file:

Typescript
'use client';

import { useState } from 'react';

export default function LikeButton() {
  const [liked, setLiked] = useState(false);

  return (
    <button onClick={() => setLiked(!liked)}>
      {liked ? 'Liked' : 'Like'}
    </button>
  );
}

'use client' marks the boundary where a piece of the component tree switches from server-rendered to hydrated-in-the-browser; components imported below that boundary run on the client too, but a Client Component can still receive Server-rendered components as children from above it. Reach for a Client Component specifically when you need:

  • useState, useReducer, or useEffect
  • Event handlers (onClick, onChange, ...)
  • Browser-only APIs (window, localStorage, ...)
  • A third-party library that itself depends on any of the above

Everything else — anything that's just displaying data — is better left as a (default) Server Component, since it ships zero JavaScript to the browser for that piece of the UI.

Common mistakes

  • Adding 'use client' to components that don't actually need it "just in case" — every Client Component (and everything it imports) ships its JavaScript to the browser and loses the zero-JS, server-rendered benefit by default.
  • Trying to use useState or onClick inside a Server Component — it fails, since Server Components never run in the browser and have no interactivity model at all.
  • Fetching with the default caching behavior and expecting content to always be fresh — without cache: 'no-store' or a revalidate value, a successful fetch inside a statically rendered route is cached indefinitely.