Next.js: The React Framework (App Router)Entry 2 of 16
Server and Client Components
In the App Router, every component is a Server Component by default. Adding 'use client' at the top of a file makes it, and everything it imports, a Client Component.
Notes
| Aspect | Server Component | Client Component |
|---|---|---|
| Runs | Server (or build time) only | Server for the first HTML, then the browser |
| JavaScript sent to browser | None | Yes |
| Can use | async/await, database, secrets, file system | useState, useEffect, event handlers, browser APIs |
| Cannot use | Hooks, event handlers, window | Secrets, direct database access |
'use client' marks a boundary, not a single component. Push it as deep as possible: keep the page a Server Component and make only the interactive leaf a Client Component.
A Server Component can pass a Server Component to a Client Component as children. A Client Component cannot import a Server Component directly.
What / Why / When
- What
Choosing where each component runs.
- Why
Less JavaScript in the browser and direct access to server data.
- When
Default to Server Components. Add
'use client'only for interactivity or browser APIs.- Use case
A product page rendered on the server with a client-side "Add to cart" button.
Code compare
// app/products/[id]/page.tsx — Server Component (default)import { AddToCart } from "./add-to-cart";import { getProduct } from "@/lib/products";export default async function ProductPage({ params }: { params: Promise<{ id: string }> }) { const { id } = await params; const product = await getProduct(id); // runs on the server return ( <main> <h1>{product.name}</h1> <p>{product.description}</p> <AddToCart productId={product.id} /> </main> );}// app/products/[id]/add-to-cart.tsx — Client Component"use client";import { useState } from "react";export function AddToCart({ productId }: { productId: string }) { const [added, setAdded] = useState(false); return ( <button onClick={() => setAdded(true)} disabled={added}> {added ? "Added" : "Add to cart"} </button> );}Angular chooses the render mode per route, not per component (hybrid rendering, v19+):
// app.routes.server.tsimport { RenderMode, ServerRoute } from "@angular/ssr";export const serverRoutes: ServerRoute[] = [ { path: "products/:id", renderMode: RenderMode.Server }, // HTML per request { path: "dashboard/**", renderMode: RenderMode.Client }, // browser only { path: "**", renderMode: RenderMode.Prerender }, // build time];// app.config.server.tsimport { mergeApplicationConfig, ApplicationConfig } from "@angular/core";import { provideServerRendering, withRoutes } from "@angular/ssr";import { appConfig } from "./app.config";import { serverRoutes } from "./app.routes.server";const serverConfig: ApplicationConfig = { providers: [provideServerRendering(withRoutes(serverRoutes))],};export const config = mergeApplicationConfig(appConfig, serverConfig);Inside a server-rendered route, every component renders on the server and hydrates in the browser. To limit what becomes interactive, and when, use @defer with incremental hydration:
<h1>{{ product().name }}</h1><p>{{ product().description }}</p>@defer (hydrate on interaction) {<app-add-to-cart [productId]="product().id" />}Browser-only code goes in afterNextRender(), which never runs on the server.
Differences and gotchas
- Next.js picks per component; Angular picks per route. Angular has no component that renders on the server and ships zero JavaScript.
- Incremental hydration is the closest Angular tool: the component's code downloads and hydrates only when its trigger fires.
- A common Next.js mistake: putting
'use client'on the page file, which turns the whole page tree into client code.
Quick reference
| Next.js | Angular |
|---|---|
| Server Component (default) | Route with RenderMode.Server or Prerender |
'use client' | Route with RenderMode.Client; @defer (hydrate on …) inside a route |
window only in Client Components | afterNextRender() |