Topics in this subject
React 4 min read Updated 5 Aug 2026

Server Components & Next.js

React Server Components, the use client boundary, streaming, and Next.js App Router with server actions.

React Server Components (RSC)#

Server Components render on the server and ship their result (a serialized RSC payload) to the client — zero client JS for that component. They can read the filesystem, hit a database, or await directly, but cannot use state, effects, refs, or browser APIs.

Server Component Client Component
Runs Server (build/request) Server (SSR) + browser (hydrate)
useState/useEffect
Event handlers (onClick)
async/await in body
Access DB/filesystem/secrets
Ships JS to client No Yes
Default in App Router Opt-in via "use client"
// app/page.jsx — Server Component by default
import db from "@/lib/db";

export default async function Page() {
  const posts = await db.post.findMany();   // runs on the server only
  return (
    <main>
      {posts.map((p) => <Article key={p.id} post={p} />)}
      <LikeButton />   {/* a Client Component for interactivity */}
    </main>
  );
}

The "use client" boundary 🎯#

"use client" at the top of a file marks it (and its import subtree) as a Client Component. It's a boundary, not a per-component tag: everything imported into a client module is also client. Server Components can render Client Components and pass serializable props; the reverse (importing a Server Component into a client one) isn't allowed, but you can pass a server component as children/props.

"use client";
import { useState } from "react";

export default function LikeButton() {
  const [likes, setLikes] = useState(0);
  return <button onClick={() => setLikes((l) => l + 1)}>👍 {likes}</button>;
}

⚠️ Props crossing the server→client boundary must be serializable — you can't pass functions, class instances, or Dates-as-methods. Keep client components at the leaves to minimize shipped JS. 🟢

flowchart TD
  A["Server Component tree (no client JS)"] --> B["Fetches data, renders HTML/RSC payload"]
  B --> C["Reaches 'use client' boundary"]
  C --> D["Client Component (hydrated in browser)"]
  D --> E["useState / onClick / effects work here"]

Streaming & Suspense#

The server can stream HTML in chunks. Wrap slow parts in <Suspense> with a fallback — the shell flushes immediately and slow subtrees stream in when ready, improving TTFB and perceived performance.

import { Suspense } from "react";

export default function Page() {
  return (
    <>
      <Header />                          {/* flushed immediately */}
      <Suspense fallback={<Spinner />}>
        <SlowFeed />                      {/* streams in when its data resolves */}
      </Suspense>
    </>
  );
}

Next.js App Router#

The app/ directory is file-system routed. Special files: page.jsx (route UI), layout.jsx (shared, nested, persistent wrapper), loading.jsx (Suspense fallback), error.jsx (error boundary), route.js (API handler). Components are Server Components by default.

Rendering How Use for
SSR Rendered per request Personalized/dynamic pages
SSG Rendered at build time Static content (marketing, docs)
ISR Static + periodic revalidate Mostly-static that updates occasionally

Control it per-fetch/segment: fetch(url, { cache: "force-cache" }) (static/SSG), { cache: "no-store" } (SSR per request), or { next: { revalidate: 60 } } (ISR).

// app/dashboard/layout.jsx — wraps all nested routes, preserved across navigations
export default function DashboardLayout({ children }) {
  return <section><Sidebar />{children}</section>;
}

Server actions#

"use server" marks a function that runs only on the server, callable from a form or client component — no manual API route. Great for mutations; pair with revalidatePath/revalidateTag to refresh cached data.

// app/actions.js
"use server";
import { revalidatePath } from "next/cache";

export async function addTodo(formData) {
  await db.todo.create({ data: { text: formData.get("text") } });
  revalidatePath("/todos");   // refresh the cached page
}

// in a Server Component:
<form action={addTodo}>
  <input name="text" />
  <button>Add</button>
</form>

🎯 Server actions run on the server with access to secrets/DB, are invoked via a form action or from client code, and integrate with the App Router cache — but validate/authorize inside them; they're a public endpoint.

Interview Q&A#

Q1. What can't a Server Component do? Use state, effects, refs, event handlers, or browser APIs — it doesn't run or hydrate on the client. For interactivity you cross into a Client Component via "use client".

Q2. What does "use client" actually do? It marks a module (and its import subtree) as a Client Component boundary — that code is bundled and hydrated in the browser. It's a boundary directive, not a per-component annotation.

Q3. Why do RSCs reduce bundle size? Their code never ships to the client — only the rendered output (RSC payload). Only components inside a "use client" boundary send JavaScript, so keeping interactivity at the leaves minimizes bundle size.

Q4. SSR vs SSG vs ISR in Next.js App Router? SSR renders per request (dynamic); SSG renders at build time (static); ISR serves static output but revalidates on an interval. In App Router you choose via fetch cache/revalidate options per segment.

Q5. What is a server action? A "use server" function that executes only on the server, callable from a form action or client code without a hand-written API route. Ideal for mutations, and it can revalidate cached routes — but must authorize its own inputs.