Development

React Server Components, Explained Without the Headache

A practical guide to React Server Components: what they are, how they differ from SSR, where the boundaries sit, and how to use them in Next.js without confusion.

// DD EditorialJun 20, 202611 min read

React Server Components (RSC) are one of the biggest shifts in the React model since hooks, and also one of the most misunderstood. The confusion usually comes from mixing them up with server-side rendering, which is a different thing entirely. This guide walks through what RSC actually does, where the boundaries live, and how to ship them without fighting the framework.

What Problem RSC Actually Solves

For years, every React component you wrote ran in the browser. To show a list of products, the browser downloaded a JavaScript bundle, ran it, fetched data over the network, and then rendered. That works, but it ships a lot of JavaScript and creates request waterfalls.

React Server Components flip part of that around. A server component runs only on the server, renders to a special serialized format, and sends the result to the client. Crucially, the component’s code is never shipped to the browser. If a component only formats data and outputs markup, there is no reason its logic should live in the user’s bundle.

The headline benefits:

  • Smaller client bundles, because server-only code stays on the server.
  • Direct data access, so a component can query a database or read the filesystem without an API layer.
  • Less client-side fetching, which removes a class of loading spinners and waterfalls.

The mental model that clicks for most people: server components are for fetching and structure, client components are for interactivity. If a component never needs useState, an event handler, or a browser API, it can probably be a server component.

Server Components Are Not SSR

This is the single most common mix-up, so it deserves its own section.

Server-side rendering (SSR) takes your normal components, runs them once on the server to produce an HTML string for the first paint, then hydrates them in the browser so they become interactive. Every one of those components still ships to the client.

Server components never ship to the client and never hydrate, because there is nothing interactive to hydrate. They render once, on the server, and produce a payload that React stitches into the tree.

You can and usually do use both together. A page in Next.js with the App Router renders server components on the server, and any client components inside it are also server-rendered for the first paint, then hydrated. SSR is about when and where the first HTML is produced. RSC is about which components exist on the client at all.

The Client Boundary and the "use client" Directive

By default in a framework like Next.js App Router, every component is a server component. You opt into the client with a directive at the top of the file:

"use client";

import { useState } from "react";

export function LikeButton({ initialCount }) {
  const [count, setCount] = useState(initialCount);
  return (
    <button onClick={() => setCount((c) => c + 1)}>
      {count} likes
    </button>
  );
}

Once a file has "use client", that component and everything it imports becomes part of the client bundle. The boundary is viral downward: a client component can only import other client components, not server ones.

But there is an elegant escape hatch. A server component can pass a server component to a client component as a child (or any prop). The server component renders on the server, and the client component just slots in the already-rendered result:

// Server component
import { ClientShell } from "./client-shell";
import { ServerChart } from "./server-chart";

export default function Page() {
  return (
    <ClientShell>
      <ServerChart />
    </ClientShell>
  );
}

Here ServerChart stays on the server even though it sits inside an interactive ClientShell. This composition pattern is how you keep heavy, data-bound rendering off the client while still wrapping it in interactivity.

Fetching Data the New Way

Server components can be async functions. You await directly inside the component, with no useEffect, no loading state machine, and no separate API route:

// Server component — runs only on the server
async function getOrders(userId) {
  const res = await fetch(`https://api.internal/orders?user=${userId}`);
  return res.json();
}

export default async function Orders({ userId }) {
  const orders = await getOrders(userId);
  return (
    <ul>
      {orders.map((o) => (
        <li key={o.id}>{o.title} — ${o.total}</li>
      ))}
    </ul>
  );
}

Because this runs server-side, you can safely use secrets, hit a database with a direct client, or read environment variables that should never reach the browser. To keep the UI responsive while data loads, wrap async server components in React <Suspense> and provide a fallback. The server streams the rest of the page first and fills in the boundary when its data resolves.

Things You Cannot Do in a Server Component

Keep this short list handy:

  • No state or effectsuseState, useReducer, useEffect are client-only.
  • No event handlersonClick, onChange, and friends require a client component.
  • No browser APIswindow, localStorage, and the DOM do not exist on the server.
  • No context consumed at the server boundary in the usual hook way; pass data down as props instead.

A Realistic Component Split

Imagine a product page. Here is how the responsibilities divide:

  1. Page (server) — fetches the product, reviews, and recommendations directly from the data layer.
  2. Gallery (client) — needs swipe gestures and zoom, so it is a client component.
  3. Reviews list (server) — just renders text and stars; no interactivity, keep it on the server.
  4. Add-to-cart (client) — owns quantity state and the click handler.

The win is concrete: the reviews list might involve markdown parsing or date formatting libraries. As a server component, none of those libraries ship to the browser. Only the gallery and the cart button add to the client bundle.

Where the Ecosystem Stands

RSC is a React feature, but it relies on a bundler and router integration, so in practice you adopt it through a framework. Next.js popularized it through the App Router, and it remains the most mature implementation. Other frameworks and routers have been building RSC support on top of bundlers like Vite, and React’s reference implementation continues to stabilize the wire format and APIs.

A few practical adoption notes:

  • You do not have to convert everything. Server and client components interoperate, so you can migrate page by page.
  • Server Actions complement RSC by letting client components call server functions for mutations without hand-writing an API endpoint.
  • Caching behavior is framework-specific and has changed across releases. Read your framework’s current caching docs rather than assuming, because this is the area most likely to surprise you.

Takeaway

React Server Components are not a replacement for SSR and not a new templating language. They are a way to keep non-interactive, data-heavy components entirely on the server, shrinking the client bundle and letting you fetch data inline. Start with the default of server components, reach for "use client" only where you need state, events, or browser APIs, and push the client boundary as far down the tree as you can. Get that boundary right and most of the headache disappears.

DD Editorial
DD Editorial
// DesignerDiscussion editorial team

We test tools, read the docs so you don't have to, and rank the agencies actually shipping great work.

  dd@signal:~ — subscribe.sh
$ ./join --weekly-signal
> one email a week. design intel, dev drops, agency rankings. zero noise.