Beyond Code & Karma · React 19 / App Router
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.
Chapter 01 — The Foundation
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.
"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.
The Cosmic Connection
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.
"Let consciousness render the unchanging; let matter carry only what must move — and the bundle stays light."
Chapter 02 — The Two Worlds
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 — 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> ); }
await db.query() never reaches
the browser. No useEffect + fetch waterfall, no
exposed connection string — just rendered output streamed down.
These get conflated constantly. They answer different questions: where does code run, and does its code ever reach the browser?
<div>. Everything is a Client Component.
| Capability | Server Component | Client Component |
|---|---|---|
| Where it runs | Server only (request / build) | Server (first paint) + browser |
| Ships JS to browser? | No — 0 KB | Yes — code + imports |
| Hooks / state (useState, useEffect)? | No | Yes |
| Event handlers (onClick)? | No | Yes |
Can be async / await data? | Yes — DB & API directly | No — fetch on the client instead |
| Read secrets / env vars? | Yes — stays server-side | No — runs in the browser |
Chapter 03 — The Boundary
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 "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.
// 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>
The browser asks for a route. React begins rendering the Server Component tree on the server.
Server Components run — awaiting data, reading secrets. Client Components are left as placeholders (holes), not executed here.
The rendered output streams down as the RSC payload — a description of UI plus references to the client components that must hydrate.
Only the client component bundles download and hydrate. Server parts are already final — no JS, no rehydration, instantly interactive where it counts.
Chapter 04 — Live Visualization
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.
Chapter 05 — Deep Dive
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:
| System | How it uses RSC |
|---|---|
| Next.js App Router | The app/ directory is Server Components by default; "use client" opts in. |
| React 19 | Ships the RSC protocol and Server Actions as first-class React. |
| Frameworks (Remix, etc.) | Adopting the RSC payload to cut hydration cost. |
Chapter 06 — Dharma & The Boundary
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.
// 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 🙏
Chapter 07 — Test Yourself
Three questions. No peeking at the docs. Your karma is watching.
The Enlightenment
कर्म करो, फल की चिंता मत करो