ProgrUmar Logo
Module 2: React Server Components & Data Fetching

Server Components vs Client Components

Duration: 16 mins

The RSC Mental Model

In the App Router, every component is a Server Component by default. Add "use client" at the top of a file only when you need browser APIs, event handlers, or React hooks.

Rules of Thumb

  • Fetch data → Server Component
  • Use useState / useEffect → Client Component
  • Access window or document → Client Component
  • Render heavy static markup → Server Component (zero JS sent to client)

Server Components vs Client Components

This is the lesson that makes everything click. If you've been slightly confused about why some components need "use client" and others don't, or why you can't use useState in certain files, this lesson will clear all of that up.

The Server Component vs Client Component distinction is the most important mental model in the App Router. Get this right and the rest of Next.js development feels logical and natural. Get it wrong and you'll be fighting the framework constantly.


1. The Old World — Everything Was a Client Component

Before React Server Components existed, every React component ran in the browser. When a user visited your page:

  1. The server sent a minimal HTML shell and a large JavaScript bundle.
  2. The browser downloaded and parsed all that JavaScript.
  3. React ran in the browser, executed your components, and painted the UI.
  4. If data was needed, another network request went to an API, then the UI updated again.

This works — but it has real costs. Every component, even ones that just render static text or display data from a database, ships JavaScript to the browser. Users on slow connections or low-powered devices feel this weight.


2. The New World — Two Types of Components

React 18 introduced React Server Components (RSC), and Next.js builds the entire App Router on top of them. You now have two distinct types of components:

Server Components Client Components
Where they run Only on the server On the server (initial render) + in the browser
Default in App Router? ✅ Yes ❌ No — opt in with "use client"
Can use hooks? ❌ No ✅ Yes
Can use browser APIs? ❌ No ✅ Yes
Can fetch data directly? ✅ Yes (async/await) ⚠️ Only via useEffect or a library
Can access backend directly? ✅ Yes (DB, file system, env vars) ❌ No
Sends JS to browser? ❌ Zero JS ✅ Yes
Can accept event handlers? ❌ No (onClick, onChange, etc.) ✅ Yes

3. Server Components In Depth

A Server Component is just a regular async function — or a regular function if it doesn't need to fetch anything. No special imports, no wrappers. The absence of "use client" is what makes it a Server Component:

// app/products/page.tsx — Server Component (default)
// No "use client" = runs ONLY on the server

export default async function ProductsPage() {
  // Direct database access — this never reaches the browser
  const products = await db.product.findMany({ where: { published: true } });

  return (
    <ul>
      {products.map((product) => (
        <li key={product.id}>
          <h2>{product.name}</h2>
          <p>${product.price}</p>
        </li>
      ))}
    </ul>
  );
}

What's happening here is remarkable — you're querying a database directly inside a component. No API route, no fetch call to your own backend, no loading state. The component runs on the server, the database query runs, and the resulting HTML is sent to the browser. Zero JavaScript from this component reaches the client.

What Server Components Can Do

  • Query databases directly (Prisma, Drizzle, raw SQL)
  • Read environment variables (even secret ones — they never reach the client)
  • Access the file system
  • Call internal services or microservices without CORS issues
  • Use heavy server-side libraries (parsers, crypto, etc.) without bloating the bundle
  • Render other Server Components or Client Components

What Server Components Cannot Do

  • Use React hooks (useState, useEffect, useRef, etc.)
  • Use browser APIs (window, document, localStorage)
  • Use event handlers (onClick, onChange, onSubmit)
  • Use Context with useContext (though they can be wrapped in Context providers)

4. Client Components In Depth

Add "use client" as the very first line of a file to make every component in that file a Client Component. This directive must be at the top — before any imports:

// components/AddToCartButton.tsx — Client Component
"use client"; // ← Must be the very first line

import { useState } from "react";

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

  function handleClick() {
    // Add to cart logic
    setAdded(true);
  }

  return (
    <button
      onClick={handleClick}
      className={added ? "bg-green-600" : "bg-blue-600"}
    >
      {added ? "Added to Cart ✓" : "Add to Cart"}
    </button>
  );
}

This component uses useState and onClick — both require the browser. It's a Client Component, so Next.js includes its JavaScript in the bundle and the component runs in the browser where it can respond to user interactions.

Client Components Still Server-Render First

Here's a subtle but important point: Client Components are not browser-only. Next.js pre-renders them on the server first to generate HTML for fast initial page loads and SEO. Then the JavaScript hydrates them in the browser to make them interactive.

So "Client Component" really means "a component that can run in the browser and use browser APIs" — not "a component that only runs in the browser."


5. Mixing Server and Client Components

The real power comes from combining both. You write the heavy data-fetching and logic in Server Components, and sprinkle in Client Components only where interactivity is needed.

Going back to the products example — the page is a Server Component that fetches data, but each product card has an interactive "Add to Cart" button:

// app/products/page.tsx — Server Component
import AddToCartButton from "@/components/AddToCartButton"; // Client Component

export default async function ProductsPage() {
  const products = await db.product.findMany();

  return (
    <ul>
      {products.map((product) => (
        <li key={product.id}>
          <h2>{product.name}</h2>
          <p>${product.price}</p>

          {/* Client Component embedded inside Server Component — totally fine */}
          <AddToCartButton productId={product.id} />
        </li>
      ))}
    </ul>
  );
}

The page fetches data on the server. The button handles clicks in the browser. The browser receives the full HTML with product names and prices already rendered — plus just the small JavaScript bundle needed for the button interactivity.


6. The Boundary Rule — A Critical Constraint

There's one constraint that trips up almost every developer learning RSC:

You cannot import a Server Component inside a Client Component.

Think about why — a Client Component runs in the browser. The browser has no ability to execute server-only code. So if a Client Component tried to import a Server Component that queries a database, it would be impossible to run.

// ❌ THIS DOES NOT WORK
"use client";

import ServerDataComponent from "./ServerDataComponent"; // Server Component

export default function ClientWrapper() {
  return (
    <div>
      <ServerDataComponent /> {/* ❌ Error — can't import SC inside CC */}
    </div>
  );
}

However — and this is the key insight — you can pass a Server Component to a Client Component as a prop (specifically as children or any other prop that accepts React nodes):

// ✅ THIS WORKS — passing as children
// app/dashboard/page.tsx — Server Component
import ClientWrapper from "@/components/ClientWrapper";
import ServerDataComponent from "@/components/ServerDataComponent";

export default function DashboardPage() {
  return (
    <ClientWrapper>
      <ServerDataComponent /> {/* ✅ Passed as children — works fine */}
    </ClientWrapper>
  );
}
// components/ClientWrapper.tsx — Client Component
"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} {/* Server Component renders here — works! */}
    </div>
  );
}

The parent Server Component composes everything together. The Client Component receives already-rendered content as children and just controls whether to show it — it never needs to "run" the Server Component.


7. "use client" is a Boundary, Not a Per-Component Switch

"use client" marks a boundary in the component tree. Every component imported below that boundary — in that file or in files it imports — is also treated as a Client Component, even if they don't have "use client" themselves.

// components/Modal.tsx
"use client"; // ← Boundary starts here

import ModalContent from "./ModalContent"; // Also becomes a Client Component
import ModalFooter from "./ModalFooter";   // Also becomes a Client Component

export default function Modal() { ... }

ModalContent and ModalFooter don't need their own "use client" — they inherit it. But this also means they can't use server-only features.

The practical implication: push "use client" as deep into the tree as possible. If only the button inside a card needs interactivity, don't mark the entire card as a Client Component — just mark the button.

// ❌ Too broad — entire card becomes a Client Component
// components/ProductCard.tsx
"use client";

export default function ProductCard({ product }) {
  return (
    <div>
      <h2>{product.name}</h2>   {/* Doesn't need to be client */}
      <p>{product.price}</p>    {/* Doesn't need to be client */}
      <button onClick={...}>Add to Cart</button>
    </div>
  );
}
// ✅ Better — only the button is a Client Component
// components/ProductCard.tsx — Server Component
import AddToCartButton from "./AddToCartButton"; // Client Component

export default function ProductCard({ product }) {
  return (
    <div>
      <h2>{product.name}</h2>
      <p>{product.price}</p>
      <AddToCartButton productId={product.id} />
    </div>
  );
}

8. Passing Data from Server to Client Components

Server Components can pass data to Client Components as props — but only data that can be serialised (converted to JSON). This means strings, numbers, arrays, plain objects, and booleans are all fine. Functions, class instances, and Dates (as objects) are not.

// ✅ Serialisable props — works fine
<ClientButton label="Click me" count={42} isActive={true} />

// ✅ Arrays and plain objects
<ClientList items={["a", "b", "c"]} config={{ theme: "dark" }} />

// ❌ Functions cannot be passed from Server to Client Components
<ClientButton onClick={() => console.log("hi")} /> // Error!

// ❌ Class instances cannot be passed
<ClientComponent date={new Date()} /> // Error! Pass date.toISOString() instead

If you need to pass a callback from a Server Component to a Client Component, the answer is usually a Server Action — we'll cover those in Lesson 205.


9. A Decision Framework — Which Should I Use?

When you're creating a new component, run through this checklist:

  1. Does it need useState, useEffect, useRef, or any other hook? → Client Component
  2. Does it need event handlers like onClick, onChange, onSubmit? → Client Component
  3. Does it use browser APIs like window, localStorage, navigator? → Client Component
  4. Does it use a third-party library that uses any of the above? → Client Component
  5. Does it fetch data, query a database, or use secrets? → Server Component
  6. Does it just render content from props or static data? → Server Component (keep JS bundle small)

When in doubt, start with a Server Component. If Next.js or TypeScript throws an error because you used something that needs the client, add "use client" then. This is better than the reverse — marking everything as a Client Component by default adds unnecessary JavaScript to your bundle.


10. Common Gotchas

  • Putting "use client" in the wrong place. It must be the very first line of the file — before any imports. Putting it after an import will cause an error.
  • Trying to use useState in a Server Component. You'll get a clear error: "useState is not a function" or "You're importing a component that needs useState. It only works in a Client Component." The fix is always "use client".
  • Importing server-only packages in Client Components. Packages that use Node.js APIs (like fs, path, or database drivers) will crash in the browser. Use the server-only npm package to explicitly guard them:
// lib/db.ts
import "server-only"; // ← Throws a build error if imported in a Client Component

import { PrismaClient } from "@prisma/client";
export const db = new PrismaClient();
  • Passing non-serialisable props. Passing a function or class instance from a Server Component to a Client Component causes a runtime error. Stick to plain JSON-compatible values.
  • Making too many things Client Components. If your whole app/ folder is full of "use client", you've lost most of the App Router's benefits. Be deliberate — Client Components should be the exception, not the rule.

Key Takeaways

  • Every component in app/ is a Server Component by default. Add "use client" to opt into the client.
  • Server Components run only on the server — they can query databases, read secrets, and send zero JS to the browser.
  • Client Components run on the server (for initial HTML) and in the browser (for interactivity) — they can use hooks and browser APIs.
  • You cannot import a Server Component inside a Client Component — but you can pass one as children.
  • "use client" is a boundary — everything below it in the import tree also becomes a Client Component.
  • Push "use client" as deep into the tree as possible to keep your JS bundle small.
  • Only serialisable values (strings, numbers, plain objects, arrays) can be passed as props from Server to Client Components.

Next up: Lesson 202 — Fetching Data in Server Components. You'll put this knowledge to work by learning every data fetching pattern the App Router supports — async components, the extended fetch API, and how to avoid the most common performance pitfalls.

Chat with us