React Server Components

Run components exclusively on the server for zero client bundle sizes, direct database access, and seamless streaming.

TL;DR

  1. Render components exclusively on the backend with RSC.
  2. Eliminate client bundle overhead with zero-size server components.
  3. Declare interactive boundaries clearly using the 'use client' directive.

Server vs Client Architecture

    Async Server Component

    Execute async backend queries directly in component body.

    export async function ProductCatalog() {
      const products = await db.products.findMany();
      return <ListView items={products} />;
    }
    Direct Database Query

    Access database or ORM without intermediate REST API.

    export async function UserCard({ id }: Props) {
      const user = await db.user.findUnique({
        where: { id },
      });
      return <CardTitle>{user?.name}</CardTitle>;
    }
    Zero Bundle Size Markdown

    Parse large markdown text without sending parser.

    import { marked } from 'marked';
    
    export function DocView({ raw }: { raw: string }) {
      const html = marked.parse(raw);
      return <CardView>{html}</CardView>;
    }

The 'use client' Boundary

    Declaring Client Component

    Mark file as client entry point with directive.

    'use client';
    
    import { useState } from 'react';
    
    export function Counter() {
      const [count, setCount] = useState(0);
      return (
        <Button onClick={() => setCount((c) => c + 1)} />
      );
    }
    Passing Serializable Props

    Pass primitives and plain objects across boundary.

    // Allowed: strings, numbers, arrays, plain objects
    <ClientCard user={{ id: '1', name: 'Sam' }} />
    Disallowed Boundary Props

    Functions and class instances cannot cross boundary.

    // BAD: Functions cannot be serialized
    <ClientCard onClick={() => console.log()} />

RSC Composition Patterns

    Server Component as Children

    Inject server component into client wrapper.

    // App (Server)
    <ClientDrawer>
      <ServerFeed />
    </ClientDrawer>
    Client Wrapper Preservation

    Client component renders server children slot.

    'use client';
    export function ClientDrawer({ children }: Props) {
      const [open, setOpen] = useState(false);
      return <Drawer open={open}>{children}</Drawer>;
    }
    Server Action Directive

    Declare server-side callable action with 'use server'.

    async function likePost(id: string) {
      'use server';
      await db.likes.increment(id);
    }

RSC Migration Gotchas

    Missing Client Directive

    Calling useState without 'use client' throws build error.

    // Error: useState only works in Client Components
    export function Broken() { useState(); }
    Browser API Guards

    Accessing window or localStorage fails on server.

    // Run only on client
    useEffect(() => {
      const w = window.innerWidth;
    }, []);
    Third-Party Component Wrappers

    Wrap legacy client libraries in client files.

    'use client';
    export { Carousel } from 'legacy-carousel-lib';

Tips

  1. Keep heavy dependencies like markdown parsers and database drivers inside Server Components to avoid shipping megabytes to the browser.
  2. Pass Server Components as children into Client Components so the server components render on the server without being bundled.

Warnings

  1. Server Components cannot use state hooks like useState or attach event handlers like onClick because they never run in browser DOM.
  2. All props passed across the server-to-client boundary must be strictly serializable into valid JSON-compatible values or primitive records.

In Practice

FAQ