Deep Dive 03 - React Server Components

Deep Dive 03 - React Server Components

Hey everyone! Welcome back to Namaste React Deep Dives!

React Server Components are the biggest change to React in years! A lot of developers find this confusing — but trust me, once you understand the core idea, everything clicks!

I am super excited because today we are going to learn something that very few React developers actually understand deeply. This gives you a massive edge in senior-level interviews!

What we will cover:

  • How React rendering evolved — CSR, SSR, SSG, ISR, RSC
  • What are React Server Components? (simple explanation!)
  • "use client" and "use server" directives
  • RSC vs SSR — they are completely different things!
  • How to use RSC with Next.js App Router
  • Streaming — showing content piece by piece
  • Data fetching patterns
  • When to use Server vs Client Components
  • Interview Questions

1. How React Rendering Has Evolved

Before we understand Server Components, let's understand the problem they solve. React rendering has gone through many stages!

ERA 1: Traditional Websites (1990s–2010s)
==========================================
User clicks a link
  ↓
Browser asks server: "Give me the About page"
  ↓
Server builds the FULL HTML page and sends it
  ↓
Browser shows the page

✅ Simple, SEO-friendly
❌ Every click causes a FULL PAGE RELOAD — slow and janky!


ERA 2: CSR — Client-Side Rendering (React SPAs, 2010s–2020s)
==============================================================
Browser gets an EMPTY HTML file + a big JavaScript bundle
  ↓
JavaScript runs in the browser
  ↓
React builds the page in the browser

✅ Smooth experience after load, no page reloads
❌ First load is SLOW — user stares at blank/spinner screen
❌ BAD for SEO — Google sees empty HTML!
❌ Large JS bundle — even slow phones must download it

Timeline on a slow phone:
  0ms   → Empty HTML arrives (nothing to see)
  1000ms → JS bundle finishes downloading
  1500ms → React finishes rendering
  User finally sees content after 1.5 seconds! 😩


ERA 3: SSR — Server-Side Rendering (Next.js, 2019+)
=====================================================
Browser asks server for a page
  ↓
Server runs React on the server → generates FULL HTML
  ↓
Browser shows the HTML immediately (user sees content!)
  ↓
JS bundle downloads → React "takes over" (hydration)
  ↓
Page becomes interactive

✅ Fast first paint, good SEO
❌ The ENTIRE JS bundle still downloads to the browser!
❌ "Hydration" makes the client re-do work the server already did


ERA 4: SSG — Static Site Generation (Gatsby, Next.js)
=======================================================
At BUILD TIME (before any user visits):
  Server runs React → generates HTML files
  Files saved and served from CDN

✅ Blazing fast — just serving HTML files, no server needed
❌ Content is fixed at build time — can't show live/user data


ERA 5: ISR — Incremental Static Regeneration (Next.js)
========================================================
Like SSG, but pages can automatically refresh after N seconds.
  Serve the cached page → rebuild in background → serve fresh page

✅ Fast like SSG + can update periodically
❌ Not truly real-time, complex to reason about


ERA 6: RSC — React Server Components (2023+) ← WE ARE HERE!
=============================================================
Components run on the SERVER but ZERO JavaScript is sent to the browser!
Only the interactive parts (Client Components) ship JS.

✅ Tiny JS bundles — server component code NEVER reaches the browser
✅ Direct DB access from components — no API layer needed
✅ Async/await directly in components
❌ New mental model to learn
❌ Only works in frameworks like Next.js App Router

2. What Are React Server Components?

This is the core concept. Let me make it as simple as possible!

Think of it like a restaurant kitchen:

Traditional React (Client Components):
========================================
The restaurant sends you ALL the raw ingredients + a recipe book.
You cook the meal AT HOME in your kitchen (the browser).

Problem: You have to download all the ingredients every time.
         Your kitchen (the browser) does all the hard work.


React Server Components:
========================================
The restaurant COOKS the meal in their kitchen (the server).
They send you a READY-MADE plate of food.
You don't need the recipe or the raw ingredients anymore!

Benefit: You get exactly what you need, nothing more.
         Your kitchen (the browser) does zero cooking for server components!

In simple words:

Server Components run on the server. Their code is NEVER sent to the browser. Zero JavaScript!

// OLD WAY — Client Component (code sent to browser):
function ProductCard({ product }) {
    const [isLiked, setIsLiked] = useState(false); // needs JS in browser!

    return (
        <div>
            <h2>{product.name}</h2>
            <button onClick={() => setIsLiked(!isLiked)}>
                {isLiked ? "❤️" : "🤍"} Like
            </button>
        </div>
    );
}
// This code IS in your JS bundle. Browser downloads and runs it.


// NEW WAY — Server Component (code stays on server!):
async function ProductCard({ productId }) {
    // Direct database access — no API needed!
    const product = await db.products.findById(productId);

    return (
        <div>
            <h2>{product.name}</h2>
            <p>Price: ₹{product.price}</p>
        </div>
    );
}
// This code is NEVER sent to the browser.
// No useState, no event handlers = no JS needed = smaller bundle!
Quick Comparison:
==================

SERVER COMPONENT:                    CLIENT COMPONENT:
✅ Runs on server only               ✅ Runs in browser (interactive!)
✅ Can access DB directly            ✅ Can use useState, useEffect
✅ Can use API keys safely           ✅ Can use onClick, onChange
✅ ZERO JS sent to browser           ✅ Can use browser APIs (localStorage)
❌ No useState                       ❌ JS code IS sent to browser
❌ No useEffect                      ❌ Cannot access DB directly
❌ No onClick / onChange

3. "use client" and "use server" Directives

In Next.js App Router, every component is a Server Component by default. You only add "use client" when you need interactivity!

// Server Component — NO directive needed, it's the default!
// app/ProductList.jsx

async function ProductList() {
    // Fetch data directly — runs on server, safe!
    const products = await fetch("https://api.example.com/products")
        .then(r => r.json());

    return (
        <ul>
            {products.map(p => (
                <li key={p.id}>{p.name} — ₹{p.price}</li>
            ))}
        </ul>
    );
}
export default ProductList;
// Client Component — needs "use client" because it uses useState!
// app/AddToCartButton.jsx

"use client";  // ← write this at the very top, before any imports!

import { useState } from "react";

export function AddToCartButton({ productId }) {
    const [added, setAdded] = useState(false);

    return (
        <button onClick={() => setAdded(true)}>
            {added ? "✅ Added!" : "Add to Cart"}
        </button>
    );
}

The best part — you can mix them! A Server Component can use a Client Component inside it:

// app/ProductCard.jsx — Server Component
import { AddToCartButton } from "./AddToCartButton"; // Client Component

async function ProductCard({ id }) {
    const product = await db.products.findById(id); // server-side DB access!

    return (
        <div>
            <h2>{product.name}</h2>
            <p>₹{product.price}</p>
            <AddToCartButton productId={id} />  {/* Client Component here! */}
        </div>
    );
}

// What happens:
// ProductCard renders on the server → sends HTML + a "placeholder" for the button
// Browser downloads ONLY the AddToCartButton JS → makes it interactive
// ProductCard code never reaches the browser! 🎉

Now let's talk about "use server" — this is for Server Actions:

// Server Actions = functions that run on the SERVER, called from the CLIENT
// Think of them as replacing your API routes!

// app/actions.js
"use server";  // ← everything in this file runs on the server

export async function addToCart(productId, userId) {
    await db.cart.add({ userId, productId }); // DB access on server!
    revalidatePath("/cart"); // refresh the cart page
}


// Using the Server Action from a Client Component:
"use client";
import { addToCart } from "./actions";

export function AddToCartButton({ productId, userId }) {
    return (
        <button onClick={() => addToCart(productId, userId)}>
            Add to Cart
        </button>
    );
}

// User clicks button → addToCart runs ON THE SERVER → DB updated → page refreshed!
// No manual API route needed! This replaces POST /api/cart!

4. RSC vs SSR — They Are NOT the Same Thing!

This is the most common confusion. Let me clear it up!

Think of SSR like a photocopy machine, and RSC like a smart printer:

SSR (Server-Side Rendering):
==============================
The photocopier copies EVERYTHING and sends it to you.
You then have to re-read it to understand it (hydration).

- Server renders ALL components → sends full HTML
- Browser shows HTML (fast! ✅)
- THEN: browser downloads the FULL JavaScript bundle
- React re-runs everything ("hydration") to make it interactive
- The same JS code is both on server AND in the browser bundle

Bundle size: SAME as a regular React app. Nothing saved!
Purpose: Fast first page load + SEO


RSC (React Server Components):
================================
The smart printer sends you ONLY what you need to interact with.
The rest is already done — you don't need to re-do it.

- Server Components render on server → their code NEVER sent to browser
- Only Client Components' JavaScript is sent to browser
- Server component code is gone after rendering — browser never sees it

Bundle size: MUCH SMALLER — server component code excluded!
Purpose: Eliminate unnecessary JavaScript from the browser


Can they work TOGETHER? YES!
==============================
In Next.js App Router you get BOTH:

Step 1: Server Components render → generate RSC Payload (not HTML)
Step 2: Next.js converts RSC Payload → sends full HTML to browser (this is SSR!)
Step 3: Browser shows HTML immediately (SSR benefit — fast first paint!)
Step 4: Browser downloads ONLY Client Component JS and hydrates them
Step 5: Server Component code NEVER reached the browser (RSC benefit!)

You get the fast first load of SSR AND the small bundle of RSC! 🚀

5. RSC with Next.js App Router

Next.js App Router (introduced in Next.js 13) uses Server Components by default. Here's how the folder structure works:

app/
├── layout.jsx       ← Server Component — the persistent shell (header, nav)
├── page.jsx         ← Server Component — the home page content
├── loading.jsx      ← Server Component — shows while page loads (auto Suspense!)
├── error.jsx        ← needs "use client" — error boundary
├── not-found.jsx    ← Server Component — your 404 page
│
└── restaurant/
    └── [id]/
        ├── page.jsx       ← Server Component — restaurant detail page
        └── ReviewForm.jsx ← Client Component — interactive form
// app/page.jsx — Home page (Server Component by default)
export default async function Home() {
    return (
        <main>
            <h1>Welcome to FoodRocket 🚀</h1>
            <RestaurantList />
        </main>
    );
}


// app/RestaurantList.jsx — Server Component
// No API route needed — query the DB directly!
async function RestaurantList() {
    const restaurants = await prisma.restaurant.findMany({
        orderBy: { rating: "desc" },
        take: 20,
    });

    return (
        <div className="grid">
            {restaurants.map(r => (
                <RestaurantCard key={r.id} restaurant={r} />
            ))}
        </div>
    );
}


// app/restaurant/[id]/page.jsx — Dynamic page (Server Component)
export default async function RestaurantPage({ params }) {
    const restaurant = await prisma.restaurant.findById(params.id);

    if (!restaurant) notFound(); // Next.js shows the not-found page

    return (
        <div>
            <h1>{restaurant.name}</h1>
            <p>Rating: {restaurant.avgRating} ⭐</p>
            <ReviewForm restaurantId={restaurant.id} /> {/* Client Component */}
        </div>
    );
}


// app/restaurant/[id]/ReviewForm.jsx — Client Component (needs interactivity)
"use client";
import { useState } from "react";
import { submitReview } from "./actions";

export function ReviewForm({ restaurantId }) {
    const [text, setText] = useState("");

    const handleSubmit = async (e) => {
        e.preventDefault();
        await submitReview({ restaurantId, text });
        setText(""); // clear after submit
    };

    return (
        <form onSubmit={handleSubmit}>
            <textarea
                value={text}
                onChange={e => setText(e.target.value)}
                placeholder="Write your review..."
            />
            <button type="submit">Submit Review</button>
        </form>
    );
}

Bonus — Page Metadata without react-helmet!

// app/page.jsx — just export metadata, Next.js handles the rest!
export const metadata = {
    title: "FoodRocket — Order Food Online",
    description: "Order from 500+ restaurants near you",
};

// No more <Helmet><title>...</title></Helmet>!
// Next.js reads this and puts it in the <head> automatically!

6. Streaming — Showing Content Piece by Piece

This is one of the coolest features! Instead of making the user wait for EVERYTHING to load, you can show content as it's ready.

Think of it like a buffet vs a set menu:

Set Menu (Traditional SSR):
============================
Kitchen prepares ALL dishes → waits until everything is done
→ brings everything to your table at once

You wait 10 minutes for everything. 😴


Buffet (Streaming with Suspense):
===================================
Kitchen brings food as each dish is ready!
→ Salad ready in 1 min → brings salad immediately!
→ Main course ready in 5 min → brings it when ready!
→ Dessert ready in 8 min → brings it when ready!

You start eating immediately! 🎉
// How Streaming works in Next.js with Suspense:

import { Suspense } from "react";

export default async function RestaurantPage({ params }) {
    return (
        <div>
            {/* This loads fast — no async data needed */}
            <h1>Pizza Palace</h1>

            {/* Show a skeleton while menu loads, then stream in the real menu */}
            <Suspense fallback={<p>Loading menu...</p>}>
                <MenuSection id={params.id} />
            </Suspense>

            {/* Reviews load independently — don't wait for the menu! */}
            <Suspense fallback={<p>Loading reviews...</p>}>
                <ReviewsSection id={params.id} />
            </Suspense>
        </div>
    );
}


// MenuSection fetches its own data:
async function MenuSection({ id }) {
    const menu = await getMenu(id); // takes 200ms
    return (
        <ul>
            {menu.items.map(item => <li key={item.id}>{item.name}</li>)}
        </ul>
    );
}

// ReviewsSection fetches its own data independently:
async function ReviewsSection({ id }) {
    const reviews = await getReviews(id); // takes 500ms
    return (
        <div>
            {reviews.map(r => <p key={r.id}>{r.text}</p>)}
        </div>
    );
}
What the user experiences:
============================
0ms    → Page title appears. "Pizza Palace" is visible immediately!
200ms  → Menu section loads. Skeleton disappears, real menu shows!
500ms  → Reviews load. Skeleton disappears, real reviews show!

WITHOUT streaming:
0ms    → Spinner. Nothing visible.
500ms  → Everything appears at once.

Streaming = much better user experience! 🚀

7. Data Fetching Patterns

There are 3 ways to fetch data in Server Components. Here's when to use each:

PATTERN 1: Sequential (slowest — avoid if possible!)
=====================================================
async function Page({ id }) {
    const restaurant = await getRestaurant(id); // waits 200ms
    const menu       = await getMenu(id);       // waits another 200ms
    const reviews    = await getReviews(id);    // waits another 200ms
    // Total: 600ms — they run ONE AFTER ANOTHER!
}

Think of it like:
  Making tea → waiting → making toast → waiting → making eggs
  Each step waits for the previous one to finish!


PATTERN 2: Parallel with Promise.all (faster!)
================================================
async function Page({ id }) {
    const [restaurant, menu, reviews] = await Promise.all([
        getRestaurant(id),  // starts immediately
        getMenu(id),        // starts immediately
        getReviews(id),     // starts immediately
    ]);
    // Total: ~200ms — they all run SIMULTANEOUSLY!
}

Think of it like:
  Making tea + toast + eggs ALL AT THE SAME TIME! ☕🍞🍳
  Total time = slowest single task, not sum of all!


PATTERN 3: Co-located fetch with Suspense (best for streaming!)
================================================================
// Each component fetches its own data independently.
// They all start at the same time AND can stream independently!

async function MenuSection({ id }) {
    const menu = await getMenu(id);
    return <MenuList items={menu.items} />;
}

async function ReviewsSection({ id }) {
    const reviews = await getReviews(id);
    return <ReviewsList reviews={reviews} />;
}

// Wrap in Suspense — each streams as soon as its data is ready!
// Menu shows at 200ms, Reviews show at 500ms. Not waiting for each other!

Next.js fetch caching — control how fresh your data is:

// Cache forever — like SSG (great for rarely changing data):
const data = await fetch(url, { cache: "force-cache" });

// Never cache — always fresh data:
const data = await fetch(url, { cache: "no-store" });

// Cache for 60 seconds, then refresh — like ISR!
const data = await fetch(url, { next: { revalidate: 60 } });

// This is how Next.js does SSG, SSR, and ISR — just through fetch() options!

8. When to Use Server vs Client Components

This is the most practical question! Here is a simple decision guide:

ASK YOURSELF: "Does this component need..."

onClick, onChange, form submission?
  → YES: "use client"  (needs interactivity)
  → NO:  keep it Server Component ✅

useState or useEffect?
  → YES: "use client"  (state and effects are browser-only)
  → NO:  keep it Server Component ✅

localStorage, window, document?
  → YES: "use client"  (browser-only APIs)
  → NO:  keep it Server Component ✅

None of the above?
  → Server Component! (default, no directive needed)


RULE OF THUMB:
==============
Push "use client" as LOW in the component tree as possible!

// ❌ Bad — making the whole page a Client Component:
"use client"; // now the entire page JS is in the bundle!
export default function ProductPage() {
    const [liked, setLiked] = useState(false);
    // ... all the data fetching and rendering here
}

// ✅ Good — only the interactive button is a Client Component:
// ProductPage.jsx (Server Component — no directive)
export default async function ProductPage({ id }) {
    const product = await db.products.findById(id); // server only!
    return (
        <div>
            <h1>{product.name}</h1>
            <p>₹{product.price}</p>
            <LikeButton />  {/* Only THIS small component is "use client" */}
        </div>
    );
}

// LikeButton.jsx (Client Component)
"use client";
export function LikeButton() {
    const [liked, setLiked] = useState(false);
    return <button onClick={() => setLiked(!liked)}>{liked ? "❤️" : "🤍"}</button>;
}

Interview Questions

Trust me, interviewers LOVE asking about RSC — very few candidates can answer these well!

Q: What is the difference between SSR and React Server Components?

"SSR and RSC are different things that solve different problems. SSR (Server-Side Rendering) runs all React components on the server to produce HTML so the user sees content immediately — but the ENTIRE JavaScript bundle still downloads to the browser, and React re-runs everything for hydration. RSC (React Server Components) goes further — server components never send their JavaScript to the browser at all. Only Client Components ship JS. So RSC gives you smaller bundles. In Next.js App Router, you actually get both — SSR for the fast first HTML, and RSC to keep server component code out of the bundle."

Q: What does "use client" do?

"'use client' is a directive you write at the very top of a file. It marks that file's components as Client Components, which means their JavaScript is bundled and sent to the browser. In Next.js App Router, all components are Server Components by default. You add 'use client' only when you need interactivity — useState, useEffect, onClick, or browser APIs like localStorage. The key is to push this boundary as low in the tree as possible so most of your code stays on the server."

Q: How does Streaming work with React Server Components?

"Streaming means sending the HTML to the browser in chunks instead of waiting for everything. With RSC and Suspense, you wrap slow async components in Suspense boundaries. React immediately sends the page shell — the parts that are ready — and shows the Suspense fallback (like a skeleton) for the slow parts. As each async component's data resolves, React streams that section to the browser and it replaces the skeleton. Users see content progressively instead of staring at a spinner."

Q: Can a Server Component use useState?

"No. useState, useEffect, and all other hooks that rely on the browser's runtime are not available in Server Components. Server Components run only on the server and their output is static HTML — there's no React runtime in the browser to manage state for them. If you need state or effects, you need to either make that component a Client Component with 'use client', or pass the interactive part down to a Client Component child."

Q: What is a Server Action?

"A Server Action is a function marked with 'use server' that runs on the server but can be called from a Client Component. Before Server Actions, you'd need to create an API route — like POST /api/cart — and call it with fetch. With Server Actions, you define the function in a file with 'use server' and import it directly into your Client Component. When the user clicks a button, the function runs on the server, can access the database, and can revalidate cached pages. It's like a remote procedure call from the client to the server."

Key Points to Remember

ConceptIn Simple Words
Server ComponentRuns on server only. ZERO JS to browser. Can use async/await, DB, API keys directly.
Client ComponentMarked with "use client". Runs in browser. Can use useState, useEffect, onClick.
"use client"Write at top of file to make it a Client Component. Add only when you need interactivity.
"use server"Server Actions — server functions called from the client. Replaces API routes.
RSC vs SSRSSR = fast first HTML. RSC = server component JS never sent to browser. Both work together!
StreamingShow content as it's ready using Suspense. Users see progress instead of a full-page spinner.
App Router defaultAll components in Next.js app/ folder are Server Components by default.
Best practicePush "use client" as LOW in the tree as possible. Keep most code on the server.

What's Next?

In Deep Dive 04, we master Performance Optimization — React Profiler, virtualization, memoization strategies, Web Vitals, and the patterns senior engineers use to make apps lightning fast!

Keep coding, keep learning! See you in the next one!