Next.js Interview Prep
Server and Client Components

React Server Components (RSC)

Zero JavaScript, Full Power

LinkedIn Hook

"How much JavaScript does your homepage actually need?"

I audited a Next.js app last month. The team had 47 components on their landing page. After we understood React Server Components properly, we realized 41 of them didn't need a single byte of JavaScript shipped to the browser.

The result? Bundle size dropped by 63%. Lighthouse performance score went from 72 to 96. And we didn't rewrite a single component — we just stopped marking things as "use client" that never needed to be.

React Server Components aren't just a new rendering trick. They're a fundamental shift in how React thinks about where code runs. Components that fetch data, read from a database, and render HTML — all on the server, with zero JavaScript cost on the client.

In Lesson 3.1, I break down what RSC actually is, what you can and can't do inside one, how it differs from SSR, and the mental model that will make every interview question about server components feel obvious.

Read the full lesson -> [link]

#NextJS #ReactServerComponents #RSC #React #WebPerformance #FrontendDevelopment #InterviewPrep


React Server Components (RSC) thumbnail


What You'll Learn

  • What React Server Components are and why they exist
  • The critical difference between RSC and Server-Side Rendering (SSR)
  • What you CAN do in a Server Component (direct database access, file system reads, secret keys)
  • What you CAN'T do in a Server Component (no useState, no useEffect, no browser APIs)
  • How RSC eliminates JavaScript from your client bundle for non-interactive UI
  • The component tree mental model: how Server and Client Components coexist
  • Why RSC is the default in the Next.js App Router and what that means for your architecture

The Analogy — The Architect vs. The Electrician

Think of building a house. Two kinds of workers show up every day:

The Architect works at the office (the server). They design the floor plan, choose materials, calculate structural loads, and produce detailed blueprints. Their work results in a physical structure — walls, floors, ceilings — but they never visit the construction site (the browser). Once the blueprints are delivered, the architect's job is done. Nobody needs to fly the architect to the house for it to stand. The house doesn't carry the architect inside it.

The Electrician must work on-site (the client). They wire light switches, install outlets, and connect appliances. Their work is interactive — flip a switch, the light turns on. The electrician must be present at the house for things to work. If you remove the electrician, the lights don't turn on.

React Server Components are the architect. They produce the structure (HTML) on the server, and their code never ships to the browser. The house stands without carrying the architect's tools, notebooks, or salary.

Client Components are the electrician. They handle interactivity — clicks, inputs, animations — and their JavaScript must be present in the browser.

The key insight: Most of your house is structure, not wiring. Most of your UI is static content, not interactive widgets. RSC lets you build the structure without shipping the architect's entire office to the construction site.

+-------------------------------------------------------------------+
|               THE ARCHITECT & ELECTRICIAN MODEL                    |
+-------------------------------------------------------------------+
|                                                                    |
|  SERVER (Architect's Office)          CLIENT (Construction Site)   |
|  +--------------------------+         +-------------------------+  |
|  |                          |         |                         |  |
|  |  - Design floor plan     | ------> |  Walls, floors, roof    |  |
|  |  - Calculate structure   | (HTML)  |  (rendered structure)   |  |
|  |  - Query material DB     |         |                         |  |
|  |  - Read secret blueprints|         |  + Light switches  [JS] |  |
|  |                          |         |  + Thermostat      [JS] |  |
|  |  Code stays HERE.        |         |  + Doorbell        [JS] |  |
|  |  Zero tools shipped.     |         |                         |  |
|  +--------------------------+         +-------------------------+  |
|                                                                    |
|  Server Components:                   Client Components:           |
|  Zero JS in the bundle.              JS shipped to the browser.   |
|                                                                    |
+-------------------------------------------------------------------+

What Are React Server Components?

React Server Components (RSC) are React components that execute exclusively on the server. They run once, produce HTML (or more precisely, a serialized React tree called the RSC payload), and their JavaScript code is never sent to the client's browser.

This is not a rendering strategy like SSR or SSG. It is a component type. RSC defines where a component runs, not when it runs. A Server Component can be statically rendered at build time (SSG) or dynamically rendered per request (SSR). The "Server" in "Server Component" means the code runs on the server — period.

In the Next.js App Router, every component is a Server Component by default. You don't need to add any special annotation. If you create a file and export a React component, it is a Server Component unless you explicitly mark it otherwise with "use client".

// app/about/page.tsx
// This is a Server Component by default — no annotation needed

// You can import server-only libraries without worrying about bundle size
import { marked } from 'marked'; // 35KB library — zero bytes sent to client

export default async function AboutPage() {
  // You can use async/await directly — Server Components support this natively
  const res = await fetch('https://api.example.com/about-content');
  const data = await res.json();

  // You can use heavy libraries for rendering — they run on the server only
  const htmlContent = marked(data.markdownBody);

  return (
    <main>
      <h1>{data.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: htmlContent }} />
      <p>Last updated: {new Date(data.updatedAt).toLocaleDateString()}</p>
    </main>
  );
}

// Result:
// - The "marked" library is NOT in the client JavaScript bundle
// - The fetch() runs on the server — the API key never touches the browser
// - The user receives pure HTML — zero JavaScript needed for this page

RSC vs. SSR — They Are Not the Same Thing

This is the single most common confusion candidates have in interviews, and getting it wrong is an immediate red flag. Let's clear it up permanently.

Server-Side Rendering (SSR) is a rendering strategy — it answers the question "when does this page render?" Answer: on every request. The server renders the entire React tree into HTML, sends it to the browser, and then React hydrates the page by loading JavaScript and attaching interactivity. All component code is still shipped to the client for hydration.

React Server Components (RSC) is a component type — it answers the question "where does this component's code live?" Answer: only on the server. The component's code is never sent to the browser. There is no hydration for Server Components because there is no JavaScript to hydrate.

+------------------------------------------------------------------+
|                    SSR vs RSC                                     |
+------------------------------------------------------------------+
|                                                                   |
|  SSR (Rendering Strategy):                                        |
|  - WHEN does the page render? -> Per request, on the server       |
|  - Does JS ship to client?    -> YES, for hydration               |
|  - Is the page interactive?   -> YES, after hydration             |
|  - Bundle size impact?        -> All component code in bundle     |
|                                                                   |
|  RSC (Component Type):                                            |
|  - WHERE does the code run?   -> Only on the server               |
|  - Does JS ship to client?    -> NO, zero bytes                   |
|  - Is the component interactive? -> NO, it's static output        |
|  - Bundle size impact?        -> Component code excluded           |
|                                                                   |
|  KEY: They are ORTHOGONAL concepts.                               |
|  A Server Component can be SSR'd (per request) or SSG'd (build).  |
|  SSR can render both Server and Client Components.                |
|                                                                   |
+------------------------------------------------------------------+

Think of it this way:

  • SSR = a restaurant that cooks every meal fresh per order (when), but still gives you the recipe card with the meal (JS bundle for hydration)
  • RSC = the kitchen keeps its recipes secret (where). You only get the finished dish. The recipe (component code) never leaves the kitchen.

You can combine them. A Next.js page can use SSR (render per request) while containing both Server Components (no JS shipped) and Client Components (JS shipped for interactivity). The rendering strategy and the component type are independent axes.


What You CAN Do in a Server Component

Server Components unlock capabilities that were previously impossible in React without a separate backend:

1. Direct Database Access

// app/users/page.tsx — Server Component
import { db } from '@/lib/database';

export default async function UsersPage() {
  // Query the database directly — no API route needed
  // This code runs on the server, so DB credentials stay safe
  const users = await db.query('SELECT id, name, email FROM users LIMIT 50');

  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>
          {user.name} — {user.email}
        </li>
      ))}
    </ul>
  );
}

// No API route. No fetch(). No useEffect().
// The database query runs on the server and the result is rendered into HTML.

2. File System Access

// app/docs/[slug]/page.tsx — Server Component
import fs from 'fs/promises';
import path from 'path';

export default async function DocPage({ params }: { params: { slug: string } }) {
  // Read a markdown file directly from the server's file system
  const filePath = path.join(process.cwd(), 'content', `${params.slug}.md`);
  const content = await fs.readFile(filePath, 'utf-8');

  return (
    <article>
      <div>{content}</div>
    </article>
  );
}

// fs and path are Node.js modules — they ONLY work on the server
// In a Client Component, importing fs would crash the build

3. Use Secret Keys Safely

// app/analytics/page.tsx — Server Component

export default async function AnalyticsPage() {
  // Environment variables with secrets are safe in Server Components
  // They run on the server — the secret never reaches the browser
  const res = await fetch('https://api.analytics.com/data', {
    headers: {
      'Authorization': `Bearer ${process.env.ANALYTICS_SECRET_KEY}`,
    },
  });
  const data = await res.json();

  return (
    <div>
      <h1>Site Analytics</h1>
      <p>Total visitors: {data.totalVisitors.toLocaleString()}</p>
      <p>Bounce rate: {data.bounceRate}%</p>
    </div>
  );
}

// ANALYTICS_SECRET_KEY is a server-only env var (no NEXT_PUBLIC_ prefix)
// It is NEVER included in the client bundle

4. Use Heavy Libraries at Zero Client Cost

// app/report/page.tsx — Server Component
import { PDFDocument } from 'pdf-lib';       // 400KB library
import { format } from 'date-fns';           // 75KB library  
import { highlight } from 'prismjs';          // 50KB library
import { marked } from 'marked';              // 35KB library

// Total: ~560KB of libraries
// Bytes shipped to client: 0

export default async function ReportPage() {
  const data = await fetch('https://api.example.com/report').then(r => r.json());
  const formattedDate = format(new Date(data.createdAt), 'MMMM d, yyyy');
  const renderedMarkdown = marked(data.body);

  return (
    <article>
      <h1>{data.title}</h1>
      <time>{formattedDate}</time>
      <div dangerouslySetInnerHTML={{ __html: renderedMarkdown }} />
    </article>
  );
}

// All four libraries execute on the server
// The client receives only the rendered HTML output
// Your Lighthouse score doesn't know these libraries exist

What You CAN'T Do in a Server Component

Server Components have strict limitations. These are not bugs — they are intentional constraints that make the zero-JS guarantee possible.

No React State (useState)

// THIS WILL NOT WORK in a Server Component
import { useState } from 'react'; // Build error!

export default function BrokenComponent() {
  const [count, setCount] = useState(0); // ERROR
  return <button onClick={() => setCount(count + 1)}>Count: {count}</button>;
}

// Why: State requires JavaScript running in the browser to track changes.
// Server Components produce static output — they don't persist in memory.

No Effects (useEffect, useLayoutEffect)

// THIS WILL NOT WORK in a Server Component
import { useEffect } from 'react'; // Build error!

export default function BrokenComponent() {
  useEffect(() => {
    document.title = 'Hello'; // ERROR — no browser APIs
  }, []);

  return <div>Hello</div>;
}

// Why: Effects run AFTER rendering, in the browser.
// Server Components don't exist in the browser — there is no "after rendering."

No Browser APIs

// NONE of these work in a Server Component:

// window.localStorage.getItem('theme')     — no window object
// document.getElementById('root')          — no document object
// navigator.geolocation.getCurrentPosition — no navigator object
// window.addEventListener('resize', ...)   — no event system
// new IntersectionObserver(...)            — no browser observers

// Why: Server Components run in Node.js, not in a browser.
// Node.js has no window, document, or navigator objects.

No Event Handlers

// THIS WILL NOT WORK in a Server Component
export default function BrokenComponent() {
  return (
    <button onClick={() => alert('clicked')}>  {/* Build error! */}
      Click me
    </button>
  );
}

// Why: onClick, onChange, onSubmit — all event handlers require JavaScript
// in the browser to detect and respond to user actions.
// Server Components ship zero JS, so they can't listen for events.

No Custom Hooks That Use State or Effects

// THIS WILL NOT WORK in a Server Component
import { useRouter } from 'next/navigation'; // Some hooks are client-only

export default function BrokenComponent() {
  const router = useRouter(); // ERROR in Server Component
  return <button onClick={() => router.push('/home')}>Go Home</button>;
}

// useRouter requires browser history API — client-only
// Use redirect() from 'next/navigation' in Server Components instead

Here is the complete reference:

+-------------------------------------------------------------------+
|           SERVER COMPONENT CAPABILITIES                            |
+-------------------------------------------------------------------+
|                                                                    |
|  CAN DO:                          CAN'T DO:                       |
|  +---------------------------+    +----------------------------+   |
|  | async/await               |    | useState                   |   |
|  | Direct DB queries         |    | useEffect / useLayoutEffect|   |
|  | File system (fs) access   |    | useRef (for DOM)           |   |
|  | Environment secrets       |    | onClick, onChange, etc.     |   |
|  | Heavy npm libraries       |    | window / document / nav    |   |
|  | Server-only modules       |    | localStorage / sessionStore|   |
|  | fetch() without useEffect |    | IntersectionObserver       |   |
|  | Import Client Components  |    | Web Audio, WebGL, etc.     |   |
|  | Pass serializable props   |    | Context (createContext)     |   |
|  | Access cookies/headers    |    | Browser-only hooks         |   |
|  +---------------------------+    +----------------------------+   |
|                                                                    |
|  Rule: If it needs the BROWSER, it needs "use client".             |
|  Rule: If it needs the SERVER, keep it as a Server Component.      |
|                                                                    |
+-------------------------------------------------------------------+

The Benefits — Why RSC Changes Everything

1. Dramatically Smaller Bundles

In a traditional React app, every component and every library you import is shipped to the browser as JavaScript. A markdown parser, a date formatting library, a syntax highlighter — they all end up in the bundle.

With RSC, only Client Components contribute to the bundle. Server Components are excluded entirely. For content-heavy applications (blogs, docs, e-commerce product pages, dashboards with read-only data), this means most of your code never reaches the client.

2. No Client-Server Waterfalls

In a traditional React app, the component renders in the browser, discovers it needs data, fires a fetch, waits for the response, and then renders again. Each nested component that needs data adds another round trip — a waterfall.

Traditional CSR waterfall:
Browser loads JS -> Renders App -> fetch('/api/user') -> wait...
  -> Renders Profile -> fetch('/api/posts') -> wait...
    -> Renders PostList -> fetch('/api/comments') -> wait...
      -> Finally renders everything

Three sequential round trips: Browser -> Server -> Browser -> Server -> Browser -> Server

With RSC, all data fetching happens on the server, close to the data source. The server can make all three database queries in parallel (or sequentially if needed) and send the complete result to the browser in a single response.

RSC model:
Server receives request -> queries user, posts, comments (parallel) -> renders full tree -> sends HTML

One trip: Browser -> Server -> Browser (done)

3. Direct Access to Backend Resources

No need to build API routes just to pass data from your database to your UI. Server Components can import database clients, read files, and use server-side SDKs directly. This eliminates an entire category of boilerplate code.

4. Automatic Code Splitting at the Component Boundary

The "use client" directive acts as a natural code-splitting boundary. Every time you mark a component with "use client", Next.js knows that everything above it (Server Components) should be excluded from the client bundle, and everything below it (the Client Component and its client imports) should be included. You don't need React.lazy() or dynamic() for basic code splitting — the architecture handles it.


The Component Tree Mental Model

This is the mental model you need for interviews. Visualize your component tree as a waterfall flowing from server to client:

                    SERVER BOUNDARY
                    ==============
                    
        +---------------------------+
        |     Layout (Server)       |   <- No JS shipped
        |                           |
        |  +---------------------+  |
        |  |   Header (Server)   |  |   <- No JS shipped
        |  +---------------------+  |
        |                           |
        |  +---------------------+  |
        |  | ProductInfo (Server)|  |   <- No JS shipped, fetches from DB
        |  |                     |  |
        |  | +-----------------+ |  |
        |  | | Price (Server)  | |  |   <- No JS shipped
        |  | +-----------------+ |  |
        |  |                     |  |
  ======|==|= CLIENT BOUNDARY ==|==|======= "use client" =========
        |  |                     |  |
        |  | +-----------------+ |  |
        |  | | AddToCart       | |  |   <- JS shipped (needs onClick)
        |  | | (Client)        | |  |
        |  | +-----------------+ |  |
        |  |                     |  |
        |  | +-----------------+ |  |
        |  | | ImageGallery   | |  |   <- JS shipped (needs swipe/zoom)
        |  | | (Client)        | |  |
        |  | +-----------------+ |  |
        |  +---------------------+  |
        |                           |
        |  +---------------------+  |
        |  |   Footer (Server)   |  |   <- No JS shipped
        |  +---------------------+  |
        +---------------------------+

Key rules of the tree:

  1. Server Components can import and render Client Components. The Layout (Server) can render AddToCart (Client) without any issues.

  2. Client Components CANNOT import Server Components. Once you cross the "use client" boundary, everything below is client territory. You can't go back to the server.

  3. You CAN pass Server Components as children (props) to Client Components. This is the "composition pattern" — the Server Component is rendered on the server, and its output (HTML) is passed as a prop.

// This works! Server Component passed as children to Client Component
// app/product/page.tsx (Server Component)
import ClientWrapper from './ClientWrapper';
import ServerContent from './ServerContent';

export default function ProductPage() {
  return (
    <ClientWrapper>
      {/* ServerContent renders on the server, its OUTPUT is passed as children */}
      <ServerContent />
    </ClientWrapper>
  );
}
// app/product/ClientWrapper.tsx
'use client';

import { useState } from 'react';

export default function ClientWrapper({ children }: { children: React.ReactNode }) {
  const [isOpen, setIsOpen] = useState(true);

  return (
    <div>
      <button onClick={() => setIsOpen(!isOpen)}>Toggle</button>
      {isOpen && children}  {/* children is pre-rendered Server Component output */}
    </div>
  );
}
// app/product/ServerContent.tsx (Server Component — no "use client")
import { db } from '@/lib/database';

export default async function ServerContent() {
  // This runs on the server even though it's rendered inside a Client Component
  const product = await db.query('SELECT * FROM products WHERE id = 1');

  return (
    <div>
      <h2>{product.name}</h2>
      <p>{product.description}</p>
    </div>
  );
}

// The composition pattern:
// 1. ServerContent executes on the server, produces HTML
// 2. That HTML is serialized and passed as the "children" prop
// 3. ClientWrapper receives pre-rendered HTML, not the Server Component function
// 4. ServerContent's code is NOT in the client bundle

React Server Components (RSC) visual 1


Common Mistakes

1. Adding "use client" to every component "just to be safe."

This is the most common mistake developers make when migrating to the App Router. If you mark every component as a Client Component, you lose every benefit of RSC — your entire codebase ships as JavaScript to the browser, exactly like a traditional React app. The rule is simple: only add "use client" when the component genuinely needs state, effects, or event handlers. If it just renders props and fetches data, leave it as a Server Component.

2. Confusing RSC with SSR and using them interchangeably in an interview.

When an interviewer asks "What are React Server Components?", answering "It's when the page renders on the server" is wrong. SSR is a rendering strategy (when). RSC is a component type (where). A Server Component can be statically rendered at build time — that is SSG, not SSR. Make sure your answer distinguishes the two axes: "RSC defines where code executes — only on the server, with zero JS shipped. SSR defines when a page renders — on every request. They're independent concepts that can be combined."

3. Trying to import a Server Component inside a Client Component.

Once you add "use client" to a file, every import in that file becomes part of the client bundle. If you try to import a Server Component that uses fs or db, the build will fail because those modules don't exist in the browser. Instead, use the composition pattern: pass the Server Component as children or a prop from a parent Server Component.


Interview Questions

1. "What are React Server Components, and how do they differ from Server-Side Rendering?"

React Server Components are components that execute exclusively on the server. Their JavaScript code is never shipped to the client browser — the client receives only the rendered output (HTML or the RSC payload). This means any libraries, database queries, or secrets used inside an RSC never enter the client bundle.

SSR, by contrast, is a rendering strategy where the entire page (including Client Components) is rendered to HTML on the server per request — but all the component JavaScript is still sent to the browser for hydration. SSR answers "when does the page render?" (per request). RSC answers "where does this component's code live?" (server only). They are orthogonal — you can have SSR that renders both Server and Client Components, or SSG that pre-renders Server Components at build time.

2. "Why can't you use useState or useEffect in a Server Component?"

useState requires persistent memory in the browser — when the user clicks a button, React needs to update state and re-render. Server Components don't exist in the browser; they run once on the server, produce output, and are done. There is no runtime to hold state or re-render.

useEffect runs after the component mounts in the browser DOM. Server Components never mount in the browser — they produce HTML on the server and their code is discarded. There is no lifecycle to hook into because the component has no client-side lifecycle.

Both hooks fundamentally require a browser runtime, which is exactly what Server Components eliminate to achieve their zero-JS guarantee.

3. "How do Server Components and Client Components coexist in the same component tree?"

The component tree flows from server to client. Server Components sit at the top and can import and render Client Components below them. When Next.js encounters a "use client" directive, it draws a boundary — everything in that file and its client-side imports becomes part of the client bundle.

Server Components cannot be imported into Client Components directly. However, you can pass Server Component output as children or props to Client Components using the composition pattern. The Server Component renders on the server, and its output (serialized HTML) is threaded into the Client Component as a prop. This lets you wrap server-rendered content with client-side interactivity without shipping the server code to the browser.

4. "A developer on your team added 'use client' to a component that only renders a list of products fetched from a database. What would you tell them?"

I would explain that by adding "use client", the component now ships its JavaScript to the browser, and it can no longer directly query the database (since that code would need to run in the browser, which has no database access). The component would need to use useEffect + fetch to get the data through an API route — adding complexity, latency (client-server waterfall), and bundle size.

Since the component only renders data and has no interactivity (no state, no events), it should remain a Server Component. As a Server Component, it can query the database directly, the library code stays out of the bundle, and the user receives pre-rendered HTML instantly. I would remove the "use client" directive and convert the data fetching from useEffect to a direct async query.

5. "What is the RSC payload, and how does it differ from HTML?"

The RSC payload is a special serialized format that React uses to represent the output of Server Components. It is not raw HTML — it is a compact, streamable description of the React tree that includes rendered Server Component output, placeholders for where Client Components should be mounted, and the props that Client Components need.

When the browser receives the RSC payload, React on the client uses it to construct the DOM. For Server Component parts, it simply inserts the rendered output. For Client Component parts, it loads the JavaScript, creates the component instances, and hydrates them. This two-phase approach is what allows Server Components to produce zero-JS output while Client Components in the same tree still get full interactivity.


Quick Reference -- Cheat Sheet

ConceptKey Point
RSC is...A component type that runs only on the server — zero JS sent to client
Default in App RouterYES — every component is a Server Component unless marked "use client"
Can doasync/await, DB queries, fs reads, secret env vars, heavy libraries
Can't douseState, useEffect, onClick, window, document, browser APIs
JS bundle impactServer Component code is excluded from client bundle entirely
RSC vs SSRRSC = where code runs (server only). SSR = when page renders (per request). Independent axes.
Composition patternPass Server Components as children to Client Components
RSC payloadSerialized React tree format — not HTML, not JSON — streamed to client
Key benefitSmaller bundles, no waterfalls, direct backend access, automatic code splitting
Key ruleOnly add "use client" when you need state, effects, or event handlers
+-----------------------------------------------+
|          RSC MENTAL MODEL                      |
+-----------------------------------------------+
|                                                |
|  1. All components are Server by default       |
|  2. Server Components run on the server        |
|  3. Their code is NEVER sent to the browser    |
|  4. They CAN: fetch data, read DB, use secrets |
|  5. They CAN'T: use state, effects, events     |
|  6. Add "use client" ONLY when needed          |
|  7. Client Components are the exception,       |
|     not the rule                               |
|                                                |
|  Decision rule:                                |
|  "Does this component need INTERACTIVITY?"     |
|    Yes -> "use client"                         |
|    No  -> Server Component (default)           |
|                                                |
|  RSC != SSR                                    |
|  RSC = WHERE (server only)                     |
|  SSR = WHEN (per request)                      |
|                                                |
+-----------------------------------------------+

Prev: Lesson 2.5 -- The Rendering Decision Framework Next: Lesson 3.2 -- Client Components & "use client" ->


This is Lesson 3.1 of the Next.js Interview Prep Course -- 8 chapters, 33 lessons.

On this page