Server Rendering, Static Rendering, and Server Components

First lesson of the Next.js (Server Rendering) course, free to read: about 23 minutes, 3 exercises and 3 quiz questions. This is the catalogue's base lesson; in the student space, it is rewritten for your profile.

Objectives

  • Distinguish client-side rendering, static rendering, partial prerendering, and incremental regeneration
  • Explain the difference between a Server Component and a Client Component in the App Router
  • Place the "use client" directive in the right spot to limit the JavaScript sent to the browser

Server-side rendering (SSR)Static rendering and ISRServer and Client Components

Why server rendering

A React application created with Vite sends the browser an almost empty HTML page and a large JavaScript file: it is the browser that builds the interface. This causes three problems: the first paint waits for the JavaScript to download, search engines see the content less well, and all data goes through API calls from the client.

Next.js is a React framework that produces the HTML on the server. The browser receives an already filled page, then React “hydrates” the interactive parts. This course uses the App Router (app/ folder), the recommended router since Next.js 13.

npx create-next-app@latest ma-boutique     # Node.js 20.9 minimum, LTS 26 recommended
cd ma-boutique
npm run dev      # http://localhost:3000

With the default answers, you get Next.js 16.4 and React 19.3, in TypeScript, with the App Router, Tailwind CSS 4, ESLint, and the Turbopack compiler. The generated next.config.ts enables cacheComponents: true: this is the rendering and caching model of Next.js 16, the one used in this course.

Rendering modes

With Cache Components, Next.js tries to prerender everything at next build. What depends on the request is rendered on demand, and only that part:

Mode When the HTML is produced Symbol Example
Static once, at build time ○ “About” page, legal notices
Partial prerendering a shell at build time, the rest on each request ◐ customer area: fixed header, personalized greeting
Incremental regeneration (ISR) at build time, then regenerated in the background (lesson 3) ○ + duration product catalog
Dynamic entirely on demand ƒ API that reads the request (lesson 4)

Request-specific data (cookies(), headers(), the URL’s searchParams, or connection() which explicitly signals “render at request time”) is read in a component placed under a <Suspense> boundary. At build time, Next.js produces the HTML of everything above it, with the fallback in place of the dynamic part: this is the static shell. On each request, it sends this shell right away and then the dynamic part in the same response. This is partial prerendering (Partial Prerendering):

// app/compte/page.tsx
import { Suspense } from "react";
import { cookies } from "next/headers";

async function Salutation() {
  const session = (await cookies()).get("session");   // request data
  return <p>{session ? "Bonjour" : "Invité"}</p>;
}

export default function Compte() {
  return (
    <main>
      <h1>Mon compte</h1>                       {/* in the static shell */}
      <Suspense fallback={<p>Chargement…</p>}>
        <Salutation />                          {/* rendered on each request */}
      </Suspense>
    </main>
  );
}

If the page reads cookies() outside any <Suspense>, next build fails: “Next.js encountered uncached or runtime data during prerendering”, with possible fixes (add <Suspense>, cache the data with "use cache"). Likewise, a value that changes on every render, like new Date() or Math.random(), is rejected during prerendering: you cache it, or compute it at request time after await connection(), under <Suspense>.

The next build command displays each route’s mode with these symbols and their legend.

Server Components by default

In app/, every component is a Server Component by default. It runs on the server, can be async and read data directly (database, API, file), and its code is never sent to the browser.

// app/produits/page.tsx — Server Component
import { listerProduits } from "@/lib/catalogue";

export default async function PageProduits() {
  const produits = await listerProduits();   // SQL query or API call, server side
  return (
    <ul>
      {produits.map((p) => (
        <li key={p.id}>{p.nom} : {p.prix} €</li>
      ))}
    </ul>
  );
}

A secret key used here stays on the server. However, a Server Component cannot use useState, useEffect, or onClick: it doesn’t exist in the browser.

Client Components and the “use client” directive

For interactivity, you write a Client Component: a file that starts with the "use client" directive.

// app/produits/BoutonFavori.tsx
"use client";
import { useState } from "react";

export function BoutonFavori({ nom }: { nom: string }) {
  const [actif, setActif] = useState(false);
  return (
    <button aria-pressed={actif} onClick={() => setActif(!actif)}>
      {actif ? "Remove" : "Add"} {nom} to favorites
    </button>
  );
}

The directive marks a boundary: this file and everything it imports go into the browser’s JavaScript. A Client Component is still prerendered to HTML on the server, then hydrated.

Golden rule: place "use client" as low as possible in the tree. The page stays on the server, only the button becomes client:

// app/produits/page.tsx (server) imports a client island
import { BoutonFavori } from "./BoutonFavori";
// … <li key={p.id}>{p.nom} <BoutonFavori nom={p.nom} /></li>

What crosses the boundary

Props passed from a Server Component to a Client Component are serialized to travel to the browser. Strings, numbers, booleans, arrays, and simple objects pass; a function does not (except a Server Action, lesson 4). A Client Component also cannot import a Server Component, but it can receive one already rendered via children:

<Accordeon>            {/* Client Component */}
  <DetailsProduit />   {/* Server Component passed as children */}
</Accordeon>

Streaming

Server rendering is split up by the <Suspense> boundaries: Next.js first sends the shell (header, navigation, fallback), then each piece as soon as its data is ready, in the same HTML stream. The user sees the page right away, even if a slow part is still loading.

Key takeaways

  • Static ○, partial prerendering ◐, ISR or dynamic ƒ: next build tells you which.
  • Request data under <Suspense>, otherwise the build fails.
  • Server Component by default: data and secrets on the server, zero JavaScript sent.
  • "use client" for state and events, at the bottom of the tree, with serializable props.

Exercises

Solutions and tests stay on the server. In the student space, your code is run and checked, and the tutor gives hints before the solution.

  1. CodeDifficulty 2JavaScript in the browser2 hints

    Choose the rendering mode (simplified model)

    Write modeDeRendu(route) which reproduces, in a simplified way, the Next.js 16 decision with Cache Components. route is an object:

    • lectures: array of the request data read by the page ("cookies", "headers", "searchParams", "connection", "params"), possibly empty;
    • dansSuspense: true if these reads happen in a component placed under <Suspense>;
    • staticParams: true if the page exports generateStaticParams (the params are then known at build time and do not count as a request read);
    • revalidate: the revalidate duration (in seconds) of the cacheLife of a page cached with "use cache", or undefined.

    Rules, in order:

    1. if at least one request read remains: "partiel" if it is under <Suspense>, otherwise "erreur" (the build fails);
    2. revalidate a strictly positive number → "isr";
    3. otherwise → "statique".
    function modeDeRendu(route) {
      // TODO
    }
  2. TaskDifficulty 22 hints

    Read the output of next build

    Create a project with npx create-next-app@latest rendu-demo, accepting the default answers (TypeScript, ESLint, Tailwind CSS, App Router, no src folder). Add two pages:

    1. app/a-propos/page.tsx that returns <h1>À propos</h1>;
    2. app/compte/page.tsx: an <h1>Mon compte</h1> followed by a <Suspense fallback={<p>Chargement…</p>}> that wraps an async component Salutation. It reads (await cookies()).get("session") (imported from next/headers) and displays Bonjour if the cookie exists, Invité otherwise.

    Run npm run build and read the routes table displayed at the end. Then move the cookies() read directly into the page, outside the <Suspense>, rerun the build and read the error, then go back to the version with <Suspense>.

    • The build finishes without errors
    • The /a-propos route is marked with the ○ (Static) symbol
    • The /compte route is marked with the ◐ (Partial Prerender) symbol
    • Without <Suspense>, next build fails, reporting request data read during prerendering
  3. Multiple choiceDifficulty 21 hint

    Where should "use client" go?

    A product page displays a title, a description, and a customer review loaded on the server, plus an “Add to cart” button that updates a counter with useState. Where should the "use client" directive go?

    • At the top of app/produits/[slug]/page.tsx
    • Only at the top of the Add to cart button's file
    • In the root layout so that everything works
    • Nowhere: useState works in a Server Component

End-of-lesson quiz

  1. What type is a component in app/ with no particular directive?

    • Client Component
    • Server Component
    • Static component with no rendering
  2. With Cache Components, what happens if a page reads cookies() outside any ?

    • The page becomes dynamic with no other message
    • next build fails and suggests adding <Suspense> or "use cache"
    • The cookie is ignored at build time
  3. Why can’t you pass onClick={() => …} from a Server Component to a Client Component?

    • Functions are not serializable
    • onClick is reserved for HTML tags
    • Next.js forbids arrow functions

The rest of the course

  1. Routing with the App Router20 min
  2. Loading Data and Mastering the Cache22 min
  3. Built-in APIs: Route Handlers and Server Actions24 min
  4. Building and Deploying a Next.js Application20 min

Next.js (Server Rendering) · Advanced · 1 h 49 min