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
-
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 asuse()
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 likeuseState
/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”
Aspect | Classic API → Client React | React Server Components |
---|---|---|
Delivery Path | Two-pass render
| One-pass stream |
JavaScript shipped | Full UI bundle for every component (static or not) | JS only for islands marked "use client" ; server-only parts ship 0 KB |
Layers you maintain | API layer + React views | Single 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.
Aspect | Benefits | Drawbacks |
---|---|---|
Bundle Size | Server-only logic ships 0 KB to client. | Still need hydration code for interactive parts. |
Data Fetching | Run DB queries directly in component—no extra API layer. | Tight coupling to backend; harder to reuse in non-server contexts. |
Streaming UX | Faster “time-to-first-byte” via HTML streaming. | Requires framework support + CDN that doesn’t buffer. |
Learning Curve | Familiar 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.
- Wrap only interactive islands (
- Cache Smartly
- Use framework caching (fetch with
{ cache: 'force-cache' }
in Next.js) to avoid redundant server fetches. - Memoize computed values when possible.
- Use framework caching (fetch with
- Error Handling & Suspense
- Combine Suspense with error boundaries so partial UI streams without crashing entire pages.
Common Pitfalls
Pitfall | Cause | Fix |
---|---|---|
Accidentally importing a client-only lib (e.g., window , localStorage ) inside a .server.tsx | Server components run on Node; browser globals are undefined | Use conditional dynamic import in a client component |
Waterfall fetches | Server component awaits sequential requests | Promise.all or parallel fetch patterns |
Large shared context between server & client | React context can’t cross the server-client boundary | Pass 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.