React Server Components

July 31, 2025

Overview

React Server Components (RSC) let you execute parts of your React tree wholly on the server and stream the result to the browser as a lightweight React Flight payload that frameworks (e.g., Next.js 14) progressively convert to HTML. Because the JavaScript for those server-only pieces never reaches the client, bundles shrink, first contentful paint arrives faster, and backend resources—databases, file-system access, environment secrets—can be called straight from the component. By default every file is a server component; add the "use client" directive to opt individual components into hydration, letting you mix server and client pieces in one tree and ship only the JS your page really needs.

Table of Contents

How React Server Components Work
How RSC Differs from “Just Calling APIs”
Basic File & Folder Conventions
Example: Mixing Server & Client Components
Best Practices
Common Pitfalls
Where React Server Components Shine
Conclusion

How React Server Components Work
^

  • Server Components (default behavior; optional .server.js/.tsx suffix)
    Executed exclusively on the server. They must not contain event handlers or browser-only APIs, but in React 19+ they can call data hooks such as use() for async fetching. The server serializes their output into a compact React Flight stream—metadata + props—not raw HTML—and begins sending it to the browser immediately.

  • Client Components (opt-in via "use client" or the legacy .client.(js|tsx) suffix)
    Shipped to the browser and hydrated there, so they may use hooks like useState/useEffect, attach event handlers, and access the DOM.

  • Streaming UI
    Because Flight packets arrive chunk-by-chunk, the framework (e.g., Next.js) can progressively convert them to HTML and paint partial UI as soon as each segment is ready—provided your CDN or proxy doesn’t buffer the response.

  • Zero-Bundled JS for Server-only Code
    Logic confined to server components—DB queries, auth checks, markdown rendering—never reaches the client bundle, keeping shipped JavaScript to an absolute minimum.

How RSC Differs from “Just Calling APIs”
^

AspectClassic API → Client ReactReact Server Components
Delivery PathTwo-pass render
  1. server sends HTML shell →
  2. browser fetches API → receives JSON → React hydrates
    (UI visible only after both passes)

One-pass stream
single RSC request → server queries DB → streams
Flight data + (optional) HTML → browser paints immediately;
only islands hydrate.

JavaScript shippedFull UI bundle for every component (static or not)JS only for islands marked "use client"; server-only parts ship 0 KB
Layers you maintainAPI layer + React viewsSingle React layer (server + client components); no extra API for server-only UI
You still need a server (Node, Edge, or serverless) to execute server components, but RSC collapses the extra API hop and removes unused JS from the client bundle.
*Traditional SSR + hydration also uses a single request, but the browser still downloads and executes the full bundle before the page is interactive.

Basic File & Folder Conventions
^

Most frameworks (Next.js 13+, Remix future releases) follow this pattern:

app/
  layout.tsx                // can be a server component
  page.tsx                  // default route component
  components/
    Header.client.tsx
    ProductList.server.tsx
  • .server.tsx / .server.jsx → server-only.
  • .client.tsx / .client.jsx → shipped and hydrated.

Example: Mixing Server & Client Components
^

// ProductList.server.tsx
import { Suspense } from "react";
import ProductCard from "./ProductCard.client";

async function fetchProducts() {
  const res = await fetch("https://fakestoreapi.com/products");
  return res.json();
}

export default async function ProductList() {
  const products = await fetchProducts();        // runs on server
  return (
    <ul className="grid grid-cols-2 gap-6">
      {products.map((p) => (
        <li key={p.id}>
          <ProductCard product={p} />            {/* client component */}
        </li>
      ))}
    </ul>
  );
}

// ProductCard.client.tsx
"use client";                                     // opt-in to client
import { useState } from "react";

export default function ProductCard({ product }) {
  const [added, setAdded] = useState(false);
  return (
    <div className="border p-4">
      <h3>{product.title}</h3>
      <p>${product.price}</p>
      <button
        onClick={() => setAdded(true)}
        disabled={added}
        className="mt-2 px-2 py-1 bg-blue-600 text-white"
      >
        {added ? "Added!" : "Add to Cart"}
      </button>
    </div>
  );
}
  • Data fetching happens on the server—no API key exposed.
  • Interactivity (Add to Cart) stays in the client component, shipping only that component’s JS.
AspectBenefitsDrawbacks
Bundle SizeServer-only logic ships 0 KB to client.Still need hydration code for interactive parts.
Data FetchingRun DB queries directly in component—no extra API layer.Tight coupling to backend; harder to reuse in non-server contexts.
Streaming UXFaster “time-to-first-byte” via HTML streaming.Requires framework support + CDN that doesn’t buffer.
Learning CurveFamiliar JSX, same component model.Need to reason about server vs. client boundaries & async components.

Best Practices
^

  • Keep Server Components Pure
    • No useState, no browser APIs, no event handlers.
    • Perfect for data loading, formatting, auth gating.
  • Minimize Client Boundaries
    • Wrap only interactive islands (<button>s, form wizards) with “use client”.
    • Fewer client bundles -> faster pages.
  • Cache Smartly
    • Use framework caching (fetch with { cache: 'force-cache' } in Next.js) to avoid redundant server fetches.
    • Memoize computed values when possible.
  • Error Handling & Suspense
    • Combine Suspense with error boundaries so partial UI streams without crashing entire pages.

Common Pitfalls
^

PitfallCauseFix
Accidentally importing a client-only lib (e.g., window, localStorage) inside a .server.tsxServer components run on Node; browser globals are undefinedUse conditional dynamic import in a client component
Waterfall fetchesServer component awaits sequential requestsPromise.all or parallel fetch patterns
Large shared context between server & clientReact context can’t cross the server-client boundaryPass serialized props or use cookies/headers

Where React Server Components Shine
^

  • E-commerce product grids: heavy data, minimal per-item JS.
  • Dashboards where charts are interactive islands but tables/data load server-side.
  • Content-heavy marketing pages with a few interactive widgets.

Conclusion
^

React Server Components let you push data-heavy, non-interactive work entirely to the server while still sprinkling interactivity where it counts. By understanding server vs. client boundaries, leveraging streaming, and keeping bundles minimal, you can deliver faster load times and simpler data pipelines—without abandoning the React component model your team already knows.