React logo with a server icon
Back to Blog
ReactNext.jsServer ComponentsWeb Development

Understanding React Server Components: A Practical Guide

React Server Components changed how I think about data fetching and rendering. Here's what they actually are, when to use them, and the mistakes to avoid.

Published on September 12, 20259 min read

What Are React Server Components?

React Server Components (RSC) are components that render exclusively on the server. They never ship their JavaScript to the client, never hydrate, and never re-render in the browser. They are not SSR — SSR renders components on the server *and* sends the JS to the client for hydration. RSC sends only the rendered output.

This distinction matters a lot for performance and architecture.

The Mental Model

Think of your component tree as having two zones:

  • Server zone — Components that fetch data, access databases, read env secrets, and render HTML. Zero JS shipped to the client.
  • Client zone — Components that need interactivity: event handlers, state, effects, browser APIs. Marked with "use client" at the top of the file.

In Next.js App Router, all components are Server Components by default. You opt into client-side rendering explicitly.

// Server Component (default) — no "use client"
// Can: async/await, access DB, read env vars
// Cannot: useState, useEffect, event handlers, browser APIs
async function ProductPage({ params }: { params: { slug: string } }) {
  const product = await db.products.findOne({ slug: params.slug });
  return <ProductDetail product={product} />;
}

// Client Component — needs "use client"
"use client";
function AddToCartButton({ productId }: { productId: string }) {
  const [loading, setLoading] = useState(false);
  return (
    <button onClick={() => handleAddToCart(productId, setLoading)}>
      {loading ? "Adding..." : "Add to Cart"}
    </button>
  );
}

The Performance Win

The performance benefit of RSC is real and measurable. On a product page without RSC, the browser has to:

1. Download the JS bundle.

2. Parse and execute it.

3. Fetch data from an API.

4. Re-render the component with the fetched data.

With RSC:

1. The server fetches data and renders HTML.

2. The browser receives rendered HTML immediately.

No loading spinner for the initial render. No client-side data fetch. No JS for purely presentational components.

This portfolio's blog pages use RSC — when you navigate to a post, the content is rendered server-side and arrives as HTML. No client-side fetch, no flash of loading state.

Data Fetching Patterns

RSC makes data fetching simple and co-located:

// Fetch directly in the component that needs the data
async function BlogPage() {
  const posts = await getBlogPosts(); // direct DB call or API fetch
  return (
    <main>
      {posts.map(post => <BlogCard key={post.id} post={post} />)}
    </main>
  );
}

// Parallel fetching — both requests fire simultaneously
async function DashboardPage() {
  const [user, stats] = await Promise.all([
    getUser(),
    getDashboardStats(),
  ]);
  return <Dashboard user={user} stats={stats} />;
}

No useEffect, no useState, no loading states for the initial render.

The "use client" Boundary

When you add "use client" to a component, that component and everything it imports becomes client-side JavaScript. This is the boundary between the two zones.

Common mistake: putting "use client" on a component high in the tree because one small child needs interactivity. This sends the entire subtree's JS to the browser.

Solution: Push the "use client" boundary as low (leaf-ward) as possible.

// ❌ Entire page becomes client JS because of one button
"use client";
async function ProductPage() { ... }

// ✅ Only the button is client JS
async function ProductPage() {
  const product = await getProduct();
  return (
    <article>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      <AddToCartButton productId={product.id} /> {/* client component */}
    </article>
  );
}

When NOT to Use Server Components

  • Any component with useState, useEffect, or event handlers — must be "use client".
  • Components that use browser-only APIs (window, document, localStorage).
  • Components that use React context (consumers must be client components).
  • Highly interactive UIs where the latency of a server round-trip would hurt UX.

Practical Takeaway

Default to Server Components. Only reach for "use client" when you actually need interactivity or browser APIs. The result is a faster app with less JavaScript shipped to the browser.