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 Child in Client Wrapper

    Inject a server component into a client wrapper.

    // Server
    <ClientDrawer>
      <ServerFeed />
    </ClientDrawer>
    Client Wrapper Preservation

    Client wrapper renders its server children slot.

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

    Declare a server-side callable action.

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

RSC Migration Gotchas

    Missing Client Directive

    useState without 'use client' errors at build time.

    // Error: useState needs 'use client'
    function Broken() { useState(); }
    Browser API Guards

    window and localStorage fail on the server.

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

    Wrap legacy client libraries in client files.

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

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