React Server Components: A Primer

Server Components change how we think about React. Here's what you need to know.

The Big Idea

Traditional React: All components run in the browser (client).

Server Components: Some components run on the server only.

Why It Matters

Smaller Bundle

Server Components don't ship their code to the browser.

// This imports a huge library
import { parse } from 'heavy-markdown-parser';

// But it's a Server Component, so the browser never downloads it
async function BlogPost({ slug }) {
  const post = await db.posts.find({ slug });
  const html = parse(post.content);
  return <article dangerouslySetInnerHTML={{ __html: html }} />;
}

The markdown parser stays on the server. Client bundle stays small.

Direct Data Access

No API layer needed:

// Server Component - runs on server
async function UserProfile({ id }) {
  const user = await db.users.findUnique({ where: { id } });
  return <div>{user.name}</div>;
}

Database query right in the component. No `fetch`, no API route, no loading state.

Streaming

Server Components can stream as they render:

async function Page() {
  return (
    <div>
      <Header />           {/* Instant */}
      <Suspense fallback={<Skeleton />}>
        <SlowContent />    {/* Streams in when ready */}
      </Suspense>
      <Footer />           {/* Instant */}
    </div>
  );
}

Users see content progressively.

Server vs Client Components

Server Components (default in Next.js App Router)

  • Can be async
  • Can use server-only APIs (database, file system)
  • Cannot use hooks (`useState`, `useEffect`)
  • Cannot handle events (`onClick`)

Client Components

  • Use hooks and state
  • Handle user interactions
  • Marked with `'use client'` directive
'use client';

function Counter() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}

Composition Pattern

Server Components can render Client Components, but not vice versa:

// Server Component
async function ProductPage({ id }) {
  const product = await getProduct(id);
  
  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      {/* Client Component for interactivity */}
      <AddToCartButton productId={id} />
    </div>
  );
}
// Client Component
'use client';

function AddToCartButton({ productId }) {
  const [adding, setAdding] = useState(false);
  // handle click, show loading, etc.
}

Common Mistakes

Using 'use client' Everywhere

If everything is a Client Component, you lose the benefits.

Start with Server Components. Add `'use client'` only where needed.

Passing Functions as Props

// ❌ Can't pass functions from Server to Client
<ClientComponent onAction={async () => await serverAction()} />

// ✅ Use Server Actions or pass serializable data
<ClientComponent productId={id} />

Getting Started

Next.js 13+ App Router uses Server Components by default. It's the easiest way to try them.

Back to Journal