Beyond Code & Karma · React 19 / App Router

React Server Components

Some components render on the server and ship zero JavaScript. Others cross the "use client" boundary and carry the weight of the browser bundle. The hard part isn't the two kinds — it's the cascade. Toggle a tree and watch the JavaScript appear.

Interactive Deep Dive · React 19

Two kinds of component.
One boundary between them.

React Server Components (RSC) render only on the server — once, during the request or at build time. They produce a serialized description of UI and stream it to the browser; their code is never sent to the client, so a Server Component adds 0 KB of JavaScript. Since React 19 and the Next.js App Router, components are Server Components by default — you opt a subtree into the client with "use client", never the other way.

The Secret: "use client" is not a label on one component — it's a boundary. The module that carries it, and everything it imports below, falls into the client bundle. Master the cascade and you master RSC.
Server Component
The default. Runs on the server, can be async and await a DB or API directly. Ships 0 KB JS — no hooks, no events.
Client Component
Marked "use client". Ships its code to the browser and hydrates, so it can use useState, useEffect, and onClick.
The 'use client' Boundary
A one-way door into the bundle. The directive's module — and every import beneath it — becomes client. You only write it once, at the top of the island.
The RSC Payload
Not HTML and not JSON — a compact stream describing rendered server output plus holes where client islands hydrate. The browser stitches it together.

Purusha & Prakriti — consciousness and matter

Samkhya Philosophy Parallel

In Samkhya, reality has two principles. Purusha is pure consciousness — formless, weightless, witnessing without acting. The Server Component is Purusha: it renders truth from the unseen and ships zero JavaScript. Prakriti is manifest matter — embodied, able to move and change. The Client Component is Prakriti: it carries the weight of JavaScript so it can hold state, run effects, and respond to touch. The "use client" directive is the descent into matter — once a module crosses it, everything below is bound to embodiment.

Purusha — the Server Component
Pure consciousness. Formless, weightless, 0 KB. Renders truth from the unseen and never enters the browser.
Prakriti — the Client Component
Manifest matter. Carries the weight of JavaScript so it can move — state, effects, interactivity.
"use client" — the descent
The crossing into matter. Once a module descends, everything below it is bound to embodiment — the cascade.

"Let consciousness render the unchanging; let matter carry only what must move — and the bundle stays light."

What each one can and can't do

A Server Component fetches data where the data lives — no API hop, no client loading state, no secrets leaked. Here's one awaiting the database directly:

app/page.tsx
// app/page.tsx — a Server Component (the default, no directive)
export default async function Page() {
  // runs on the server: await the DB directly, secrets stay here, 0 KB JS
  const products = await db.query("SELECT * FROM products");
  return (
    <ul>
      {products.map((p) => (
        <ProductCard key={p.id} product={p} />
      ))}
    </ul>
  );
}
Why it matters: the await db.query() never reaches the browser. No useEffect + fetch waterfall, no exposed connection string — just rendered output streamed down.

RSC vs SSR vs CSR — three different axes

These get conflated constantly. They answer different questions: where does code run, and does its code ever reach the browser?

RSC — Server Components
Render on the server, never hydrate, never ship code. A new axis: it's about which component code reaches the browser at all.
SSR — Server Rendering
Client Components rendered to HTML on the server for first paint, then hydrated in the browser. Their JS still ships.
CSR — Client Rendering
The legacy SPA: ship a JS bundle, render in the browser from an empty <div>. Everything is a Client Component.

The cheat sheet — server vs client

CapabilityServer ComponentClient Component
Where it runsServer only (request / build)Server (first paint) + browser
Ships JS to browser?No — 0 KBYes — code + imports
Hooks / state (useState, useEffect)?NoYes
Event handlers (onClick)?NoYes
Can be async / await data?Yes — DB & API directlyNo — fetch on the client instead
Read secrets / env vars?Yes — stays server-sideNo — runs in the browser

"use client" is a door, not a sticker

The single most misunderstood thing about RSC: "use client" at the top of a file does not mean "this one component is a Client Component." It marks an entry point into the client bundle — that module and every module it imports become client code.

AddToCartButton.tsx
// AddToCartButton.tsx
"use client";   // ← the boundary: this module + all it imports = client bundle

import { useState } from "react";

export function AddToCartButton({ id }) {
  const [count, setCount] = useState(0);   // hooks need the client
  return (
    <button onClick={() => setCount(count + 1)}>
      Add to cart ({count})
    </button>
  );
}

The optimization is the inverse: a Server Component can render a Client Component and pass it server-rendered children as props. Those children stay on the server — the interactive shell stays thin.

Tabs.tsx
// A Client Component can RENDER server-built children passed as props.
"use client";
export function Tabs({ children }) {   // children stay on the server
  const [open, setOpen] = useState(0);
  return <div onClick={() => setOpen(1)}>{children}</div>;
}

// page.tsx (Server) — ServerHeavy renders on the server, never shipped:
<Tabs><ServerHeavy /></Tabs>

How a request becomes pixels — the four phases

Request arrives

The browser asks for a route. React begins rendering the Server Component tree on the server.

Server renders

Server Components run — awaiting data, reading secrets. Client Components are left as placeholders (holes), not executed here.

Serialize the RSC payload

The rendered output streams down as the RSC payload — a description of UI plus references to the client components that must hydrate.

Hydrate the islands

Only the client component bundles download and hydrate. Server parts are already final — no JS, no rehydration, instantly interactive where it counts.

Watch the boundary cascade

A five-node tree. Click any node to flip it between Server and Client and watch the "use client" boundary drag everything below it into the bundle. The counter tracks the JavaScript shipped to the browser.

Component tree · click to toggle
JS shipped: ~0 KB

    The nuances that trip people up

    This is where the surface-level tutorials stop. Here's what actually bites in production.

    When a Server Component passes props into a Client Component, those props are serialized into the RSC payload. So they must survive serialization — strings, numbers, plain objects/arrays, and Server Actions are fine. You cannot pass a function, a class instance, or a Date you rely on methods of. When the client doesn't need the data, pass a children slot of server JSX instead.

    Client Components are still server-rendered to HTML for the first paint — they just also ship JS and hydrate. "Client" means "runs in the browser too," not "renders only in the browser." The only thing that never runs in the browser is a Server Component.

    Wrap a slow Server Component in <Suspense fallback={…}> and React streams the rest of the page immediately, swapping in that subtree's HTML when its data resolves — no client JS required for the wait. It's progressive rendering without a single useEffect.

    Coming from the SPA era, people assume everything is a Client Component and add "use client" everywhere. The model is inverted now: default to server, and opt into client only at the interactive leaves. Every unnecessary directive drags a subtree into the bundle.

    It's the default model in the modern React ecosystem:

    SystemHow it uses RSC
    Next.js App RouterThe app/ directory is Server Components by default; "use client" opts in.
    React 19Ships the RSC protocol and Server Actions as first-class React.
    Frameworks (Remix, etc.)Adopting the RSC payload to cut hydration cost.

    The Karma of "use client"

    The Karma Connection

    Karma is the principle that every action has a consequence that does not stay contained. In an RSC tree, the action is the "use client" vow — and its consequence cascades to every descendant, whether they asked for it or not.

    karma.tsx
    // Every "use client" is a karmic vow…
    "use client";          // this binds the whole module to the browser
    
    // …and it cascades — every child imported below inherits the weight.
    import { Child } from "./Child";   // Child is now client too — no choice

    A directive placed high in the tree binds the whole subtree to embodiment: every imported child becomes client code, ships JavaScript, and hydrates — the karmic weight of one decision, paid by many. Place the vow at the lowest leaf that truly needs to move, and the rest of the tree stays Purusha: weightless, free, rendering truth at 0 KB.

    "Bind to matter only what must move; let the rest remain pure." — the App Router, interpreted for tired frontend engineers 🙏

    RSC Trivia

    Three questions. No peeking at the docs. Your karma is watching.

    Question 1
    You add 'use client' to the top-level Page component. Its child ProductList has no directive and no hooks. What is ProductList at runtime?
    Question 2
    Which can a Server Component do that a Client Component cannot?
    Question 3 — 🌶️ Spicy
    Is a Client Component rendered to HTML on the server at all?

    The Six Truths of React Server Components

    Server is Default
    Since React 19, every component is a Server Component until you say otherwise. You opt into the client, never out of it.
    Boundary, Not Label
    "use client" marks an entry point into the bundle. The module and all it imports become client — write it once, at the top of the island.
    The Cascade
    Everything below the boundary is forced client, hooks or not. Place it high and you ship a whole subtree; place it low and most of the tree stays free.
    RSC ≠ SSR
    SSR renders client code to HTML then hydrates it. RSC code is never hydrated and never ships. A real page uses both, on different parts.
    Serializable Props
    Props crossing into a client island must survive serialization — no functions, no class instances. Pass server children as a slot instead.
    Purusha — Stay Light
    Bind to matter only what must move. Keep interactivity at the leaves, and the rest of the tree renders weightless at 0 KB.

    कर्म करो, फल की चिंता मत करो