Mark Lowel Montealto — Full Stack Developer & DevOps Engineer

Mark Lowel
Montealto

Full Stack Developer & DevOps Engineer

Blog

May 2025

·

3 min read

React Server Components Explained: When (and When Not) to Use Them

React Server Components run on the server, cut JS bundle size, and enable direct DB access — but they're not right for every component. Here's the complete picture.

Introduction

  • React Server Components (RSC) shipped in React 18/19 and are stable in Next.js App Router.
  • The fundamental shift: components can now run exclusively on the server — zero client JS emitted.
  • This article clarifies the mental model, the boundaries, and the decision framework.

The RSC Mental Model

  • By default in Next.js App Router, every component is a Server Component unless marked "use client".
  • Server Components render to HTML + a serializable RSC payload — no hydration needed.
  • Client Components still hydrate — they get the interactivity benefit with the expected JS cost.
  • The tree can mix both: a Server Component can render Client Components as children (not vice versa without workarounds).
// Server Component (default — no "use client")
async function PostList() {
  const posts = await db.query('SELECT * FROM posts'); // direct DB — no API needed
  return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>;
}
 
// Client Component
"use client";
import { useState } from 'react';
export function LikeButton({ postId }: { postId: string }) {
  const [liked, setLiked] = useState(false);
  return <button onClick={() => setLiked(l => !l)}>{liked ? '❤️' : '🤍'}</button>;
}

What Server Components Can Do That Client Components Cannot

  • await at the top level — direct database, filesystem, or external API calls.
  • Import server-only packages (ORMs, SDKs with secrets) without leaking them to the client.
  • Zero runtime JS cost for purely presentational components.
  • Access cookies(), headers() from next/headers (Next.js only).

What Server Components Cannot Do

  • Use React state (useState, useReducer) or lifecycle hooks (useEffect).
  • Attach DOM event handlers (onClick, onChange, etc.).
  • Use browser-only APIs (window, localStorage).
  • Use Context providers directly (Context must be in a Client Component wrapper).
  • Pass non-serializable props (functions, class instances) to Client Components.

The Component Decision Framework

Use this checklist to decide Server vs Client:

NeedUse
Fetch data (DB, API, fs)Server Component
Display-only UI, no interactivityServer Component
State, events, hooksClient Component
Browser APIsClient Component
Authentication/cookiesServer Component
Animations, drag-and-dropClient Component

The Composition Pattern: "Server Shell, Client Leaf"

  • Keep the data-fetching wrapper as a Server Component.
  • Pass the fetched data down as props to a "use client" child for interactivity.
  • This minimises the "client boundary" — only the interactive leaf hydrates.
// page.tsx (Server Component)
import { PostContent } from './PostContent'; // Server
import { LikeButton } from './LikeButton';   // Client
 
async function BlogPostPage({ slug }: { slug: string }) {
  const post = await getPostBySlug(slug);
  return (
    <article>
      <PostContent post={post} />
      <LikeButton postId={post.id} />   {/* only this hydrates */}
    </article>
  );
}

Avoiding the "Client Component Sprawl" Anti-Pattern

  • Marking a parent "use client" converts its entire subtree to Client Components.
  • Strategy: push "use client" as far down the tree as possible.
  • Use the children prop pattern to pass Server Component content into a Client wrapper without making it a client component.

Streaming and Suspense

  • Server Components compose naturally with <Suspense> and React's streaming.
  • Next.js loading.tsx uses Suspense under the hood — instant shell, streamed content.
  • use() hook (React 19) can suspend on a promise passed from a Server Component.

Common Mistakes

  • Putting "use client" at the layout level — makes the whole app a Client tree.
  • Trying to share component state between a Server and Client Component.
  • Importing a server-only module into a Client Component (use server-only package to throw at build time if this happens).
  • Forgetting that params and searchParams in Next.js are now async (await params).

Conclusion

  • Default to Server Components for anything that doesn't need interactivity.
  • Push "use client" to the leaves — interactive widgets, not page wrappers.
  • The result: smaller JS bundles, faster Time-to-Interactive, and simpler data fetching.
  • Next read: "Data Fetching in Next.js: Caching, Revalidation & Streaming."