Skip to content
C Codeloom
React

React Server Components Explained

A practical guide to React Server Components: how they differ from client components, the rendering model, streaming, and when to reach for each.

·5 min read · By Codeloom
Intermediate 10 min read

What you'll learn

  • What Server Components really are
  • How RSC differs from SSR
  • The client-server boundary
  • Streaming and Suspense integration
  • When to use server vs client components

Prerequisites

  • Comfortable with React hooks

React Server Components (RSC) change the default place where your components run. Instead of shipping every component to the browser as JavaScript, RSC lets you render components on the server, send a serialized output to the client, and only hydrate the bits that actually need interactivity. The result is smaller bundles, less client-side work, and a more direct path from your data layer to your UI.

What and why

A Server Component is a React component that runs only on the server. It can read from the file system, query a database, or call internal APIs without authentication tokens leaking to the browser. It cannot use useState, useEffect, refs, or event handlers because none of that is meaningful on the server. A Client Component is the React you already know: it runs in the browser, can hold state, and responds to user interaction.

The “why” is mostly performance and data locality. With traditional SPAs, the browser downloads a large bundle, mounts components, and then makes a second round trip to fetch data. With RSC, the server can render the data-heavy parts ahead of time, send minimal markup plus a payload describing the React tree, and let the client handle only the interactive islands.

Mental model

Think of your tree as a layered cake. Server Components form the base and inner layers; they handle data and structure. Client Components are sprinkled on top wherever you need interactivity. The boundary is marked by a "use client" directive at the top of a module.

Request
 |
 v
Server: render Server Components
 |     - fetch data, read DB
 |     - emit RSC payload (JSON-ish)
 v
Network: stream payload + HTML
 |
 v
Browser: parse payload
 |     - mount Client Components
 |     - hydrate interactive islands
 v
Interactive page
RSC render flow

Once a module has "use client", every component it imports is part of the client bundle. Conversely, a Server Component can render a Client Component as a child, but a Client Component cannot import a Server Component directly. It can, however, accept Server Components as children props. This pattern is the key to mixing the two.

Hands-on example

Imagine a dashboard with a server-rendered list and a client-side filter input.

// app/dashboard/page.tsx (Server Component by default)
import { db } from '@/lib/db';
import FilterBar from './FilterBar';

export default async function DashboardPage() {
  const orders = await db.orders.findMany({ take: 50 });
  return (
    <section>
      <h1>Recent orders</h1>
      <FilterBar>
        <ul>
          {orders.map((o) => (
            <li key={o.id}>{o.customer} — ${o.total}</li>
          ))}
        </ul>
      </FilterBar>
    </section>
  );
}
// app/dashboard/FilterBar.tsx
'use client';
import { useState } from 'react';

export default function FilterBar({ children }: { children: React.ReactNode }) {
  const [query, setQuery] = useState('');
  return (
    <div>
      <input value={query} onChange={(e) => setQuery(e.target.value)} />
      <div data-query={query}>{children}</div>
    </div>
  );
}

The page component runs on the server, talks to the database directly, and never ships its source to the browser. The filter input is a Client Component because it needs useState. The list rendered by the server is passed in as children, so it stays on the server side of the boundary.

RSC vs SSR

Server-side rendering produces an HTML string for the first paint, then hydrates the full client app. RSC also runs on the server but emits a richer payload that React understands natively. The two work together: RSC handles “what to render”, SSR handles “produce HTML for the first response”. You usually do not pick one; you use both, with RSC determining which parts ever reach the client.

Common pitfalls

  • Importing a server-only module (like a database client) inside a file that ends up in a Client Component. The bundler will refuse, or worse, leak the import. Keep "use client" files free of server imports.
  • Trying to pass non-serializable props across the boundary. Functions, class instances, and Dates-with-methods cannot cross. Pass plain data.
  • Forgetting that hooks like useEffect only run in Client Components. If you copy-paste a snippet that uses them into a Server Component, the build will error.
  • Overusing "use client". A common mistake is marking the root layout as a client component, which collapses the whole tree back into the bundle. Push the directive as deep as possible.

Best practices

  • Default to Server Components. Only opt into the client when you need state, effects, browser APIs, or event handlers.
  • Co-locate data fetching with the component that uses it. Async Server Components let you await directly in the render function, which removes the need for many useEffect calls.
  • Use Suspense boundaries to stream slow pieces independently. Fast parts paint immediately while slower data continues to load.
  • Keep client components small and leaf-shaped. Wrap them with server components that handle layout and data.

FAQ

Can I use RSC outside Next.js? Yes, but the supporting frameworks are limited. Next.js App Router, Waku, and Redwood are the main production options today.

Do Server Components replace API routes? Often, yes. If a page only reads data for its own render, you can skip the API layer entirely. Mutations still typically go through Server Actions or route handlers.

How does caching work? Server Components participate in framework-level caching. In Next.js, fetches inside Server Components are deduplicated and can be cached for a duration you control.

Is RSC slower because everything runs on the server? Not in practice. Less JavaScript reaches the client, the server is usually closer to your database, and streaming means users see content faster. The tradeoff is that interactivity for new data still requires a round trip.