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.

react.dev'use client'

Notes

AspectServer ComponentClient Component
RunsServer (or build time) onlyServer for the first HTML, then the browser
JavaScript sent to browserNoneYes
Can useasync/await, database, secrets, file systemuseState, useEffect, event handlers, browser APIs
Cannot useHooks, event handlers, windowSecrets, 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

Next.js
// 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>  );}
Next.js
// 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+):

Angular
// 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];
Angular
// 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:

Angular
<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.jsAngular
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 ComponentsafterNextRender()