React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

react-19server-componentsrscuse-clientserialization

Server Components - Run React components on the server with zero client-side JavaScript

Recipe

// app/page.tsx  (Server Component -- the default in Next.js App Router)
import { db } from "@/lib/db";
import ClientCounter from "./ClientCounter";
 
export default async function DashboardPage() {
  // Direct database access -- this code never reaches the browser
  const stats = await db.query("SELECT count(*) FROM orders");
 
  return (
    <main>
      <h1>Dashboard</h1>
      <p>Total orders: {stats.count}</p>
      {/* Hand off to a Client Component for interactivity */}
      <ClientCounter initialCount={stats.count} />
    </main>
  );
}
// app/ClientCounter.tsx
"use client";
 
import { useState } from "react";
 
export default function ClientCounter({ initialCount }: { initialCount: number }) {
  const [count, setCount] = useState(initialCount);
  return <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>;
}

When to reach for this: Use Server Components whenever a component only needs to read data and render HTML -- no event handlers, no state, no browser APIs.

Working Example

// A product catalog page mixing server and client concerns
// app/products/page.tsx  (Server Component)
import { Suspense } from "react";
import { getProducts, getCategories } from "@/lib/api";
import ProductGrid from "./ProductGrid";
import CategoryFilter from "./CategoryFilter";
 
export default async function ProductsPage() {
  const categories = await getCategories();
 
  return (
    <div className="flex gap-6">
      {/* Client Component for interactive filtering */}
      <CategoryFilter categories={categories} />
 
      {/* Server Component with streaming */}
      <Suspense fallback={<p>Loading products...</p>}>
        <ProductList />
      </Suspense>
    </div>
  );
}
 
async function ProductList() {
  const products = await getProducts();
  // ProductGrid is a "use client" component that receives serializable props
  return <ProductGrid products={products} />;
}
// app/products/CategoryFilter.tsx
"use client";
 
import { useState, useTransition } from "react";
import { useRouter } from "next/navigation";
 
type Props = { categories: { id: string; name: string }[] };
 
export default function CategoryFilter({ categories }: Props) {
  const [selected, setSelected] = useState<string | null>(null);
  const [isPending, startTransition] = useTransition();
  const router = useRouter();
 
  function handleSelect(id: string) {
    setSelected(id);
    startTransition(() => {
      router.push(`/products?category=${id}`);
    });
  }
 
  return (
    <aside>
      <h2>Categories</h2>
      <ul>
        {categories.map((c) => (
          <li key={c.id}>
            <button
              onClick={() => handleSelect(c.id)}
              className={selected === c.id ? "font-bold" : ""}
            >
              {c.name}
            </button>
          </li>
        ))}
      </ul>
      {isPending && <p>Filtering...</p>}
    </aside>
  );
}
// app/products/ProductGrid.tsx
"use client";
 
type Product = { id: string; name: string; price: number };
 
export default function ProductGrid({ products }: { products: Product[] }) {
  return (
    <div className="grid grid-cols-3 gap-4">
      {products.map((p) => (
        <div key={p.id} className="border p-4 rounded">
          <h3>{p.name}</h3>
          <p>${p.price.toFixed(2)}</p>
          <button onClick={() => alert(`Added ${p.name}`)}>Add to cart</button>
        </div>
      ))}
    </div>
  );
}

What this demonstrates:

  • Server Component fetching data with direct database/API access
  • Suspense streaming a slow server component
  • Passing serializable data (plain objects, arrays, strings, numbers) from server to client
  • Client Components using state and event handlers
  • The "use client" boundary directive

Deep Dive

How It Works

  • Server Components execute only on the server during rendering. Their output is a serialized React tree (RSC payload) that is streamed to the client.
  • The RSC payload is not HTML -- it is a compact binary/JSON-like format that React on the client can reconcile with the existing DOM.
  • Components are Server Components by default. A file must include "use client" at the top to opt into client-side rendering.
  • Server Components can import Client Components, but Client Components cannot import Server Components. Instead, pass Server Components as children or other JSX props.
  • Props crossing the server-client boundary must be serializable: strings, numbers, booleans, null, arrays, plain objects, Dates, Map, Set, FormData, typed arrays, Promise (with use()), server actions, and JSX elements. Functions (except server actions), classes, and DOM nodes are not serializable.
  • Server Components can be async -- they can use await directly in the function body. This is not allowed in Client Components.
  • Because Server Components ship zero JavaScript to the browser, large dependencies used only for rendering (e.g., syntax highlighters, markdown parsers) have no client bundle impact.

Variations

Async data fetching patterns:

// Pattern 1: Top-level await
async function UserProfile({ userId }: { userId: string }) {
  const user = await fetchUser(userId);
  return <h1>{user.name}</h1>;
}
 
// Pattern 2: Parallel data fetching
async function Dashboard() {
  const [users, posts, stats] = await Promise.all([
    fetchUsers(),
    fetchPosts(),
    fetchStats(),
  ]);
  return (
    <>
      <UserList users={users} />
      <PostFeed posts={posts} />
      <StatsPanel stats={stats} />
    </>
  );
}
 
// Pattern 3: Pass promise to client (defer resolution)
async function Page() {
  const dataPromise = fetchSlowData(); // do NOT await
  return <ClientChart dataPromise={dataPromise} />;
}

Composition pattern -- passing Server Components as children:

// This works because children is already-rendered JSX, not an import
"use client";
export function ClientLayout({ children }: { children: React.ReactNode }) {
  const [open, setOpen] = useState(true);
  return <div className={open ? "expanded" : "collapsed"}>{children}</div>;
}

TypeScript Notes

  • Async Server Components return Promise<JSX.Element>. TypeScript handles this automatically with React 19 types.
  • Use React.ReactNode for the children prop when a Client Component wraps Server Component output.
  • Serializable prop types should avoid functions, class instances, and Symbols.

Gotchas

  • Importing a Client Component into a Server Component is fine; the reverse is not -- A Client Component cannot import a Server Component module. Fix: Pass the Server Component as children or another JSX prop instead of importing it.
  • Hooks are not allowed in Server Components -- useState, useEffect, useRef, etc. are client-only. Fix: Move interactive logic into a "use client" component.
  • Closures do not serialize -- You cannot pass a regular function as a prop from server to client. Fix: Use a Server Action ("use server") instead, which React serializes as an RPC reference.
  • Third-party libraries may not be RSC-compatible -- Libraries that use useEffect or browser APIs will fail in Server Components. Fix: Import them only inside "use client" files or use a wrapper.
  • No access to browser APIs -- window, document, localStorage etc. do not exist on the server. Fix: Gate browser-only code behind "use client".
  • Re-rendering works differently -- Server Components re-execute on the server when the route changes. They do not re-render in response to state changes. Fix: Lift interactive state into Client Components.

Alternatives

ApproachWhen to choose
Server ComponentsRead-only UI, data fetching, large dependencies you want out of the bundle
Client ComponentsInteractive UI with state, effects, or browser APIs
Server-side rendering (SSR) without RSCPre-React 19 apps, frameworks that do not support RSC yet
Static Site Generation (SSG)Content that rarely changes and can be built at deploy time
API routes + client fetchWhen you need fine-grained control over caching and data shape

FAQs

What makes a component a Server Component vs a Client Component?
  • Components are Server Components by default in frameworks that support RSC (e.g., Next.js App Router)
  • A file must include "use client" at the top to opt into client-side rendering
  • Server Components run only on the server and ship zero JavaScript to the browser
Can a Client Component import a Server Component?
  • No. Client Components cannot import Server Component modules
  • Instead, pass Server Components as children or other JSX props to Client Components
  • A Server Component can import Client Components without restrictions
What data types can be passed as props from a Server Component to a Client Component?
  • Strings, numbers, booleans, null, arrays, plain objects, Dates, Map, Set, FormData, typed arrays
  • Promise (consumed with use()) and server actions are also serializable
  • Functions (except server actions), class instances, and DOM nodes are not serializable
Can I use useState, useEffect, or other hooks inside a Server Component?
  • No. Hooks like useState, useEffect, useRef, etc. are client-only
  • Server Components can use await directly in the function body instead
  • Move interactive logic into a "use client" component
How do you fetch data in parallel in a Server Component?
async function Dashboard() {
  const [users, posts, stats] = await Promise.all([
    fetchUsers(),
    fetchPosts(),
    fetchStats(),
  ]);
  return <>{/* render data */}</>;
}
What is the RSC payload and how does streaming work?
  • The RSC payload is a compact binary/JSON-like format (not HTML) that React on the client reconciles with the existing DOM
  • Server Components can be wrapped in <Suspense> boundaries for streaming -- the fallback shows while the component resolves
  • This allows the page to be partially interactive before all data has loaded
Gotcha: A third-party library breaks when used in a Server Component. Why?
  • Libraries that use useEffect, useState, or browser APIs (window, document) will fail in Server Components
  • Import them only inside "use client" files or create a thin client wrapper component
Gotcha: Why does my Server Component not re-render when client state changes?
  • Server Components re-execute on the server only when the route changes
  • They do not re-render in response to client-side state changes
  • Lift interactive state into Client Components if you need reactivity
How do you pass a promise from a Server Component to a Client Component for deferred loading?
// Server Component -- do NOT await
async function Page() {
  const dataPromise = fetchSlowData();
  return <ClientChart dataPromise={dataPromise} />;
}

The client component uses use(dataPromise) inside a <Suspense> boundary to resolve it.

How do you type an async Server Component in TypeScript?
  • Async Server Components return Promise<JSX.Element> -- TypeScript handles this automatically with React 19 types
  • Use React.ReactNode for the children prop when a Client Component wraps Server Component output
  • Serializable prop types should avoid functions, class instances, and Symbols
What is the composition pattern for wrapping Server Components in Client Components?
"use client";
export function ClientLayout({ children }: { children: React.ReactNode }) {
  const [open, setOpen] = useState(true);
  return <div className={open ? "expanded" : "collapsed"}>{children}</div>;
}

The children is already-rendered JSX from a Server Component, not an import.

Do large dependencies used only in Server Components affect the client bundle?
  • No. Because Server Components ship zero JavaScript to the browser, large dependencies like syntax highlighters or markdown parsers have no client bundle impact
  • This is a key performance benefit of the RSC architecture
  • Overview -- What's new in React 19
  • Server Actions -- Mutating data from server functions
  • use() Hook -- Consuming promises passed from Server Components
  • Asset Loading -- Preloading resources alongside server-rendered content