Skip to main content
Ganesh Joshi
Back to Blogs

React Server Components: what they are and when to use them

February 17, 20266 min read
Tutorials
React or server-side code on screen

React Server Components (RSC) are components that render exclusively on the server. Unlike traditional server-side rendering, they don't send component JavaScript to the client—only the rendered output. This reduces bundle size and enables direct access to server resources like databases and file systems.

How Server Components work

When you render a Server Component:

  1. React executes the component on the server
  2. The output is serialized as RSC payload (or HTML for initial load)
  3. Client receives the payload and renders it
  4. No component code ships to the browser
// This component runs only on the server
async function ProductList() {
  // Direct database access - no API needed
  const products = await db.query('SELECT * FROM products');

  return (
    <ul>
      {products.map((product) => (
        <li key={product.id}>{product.name}</li>
      ))}
    </ul>
  );
}

The database driver and query logic never reach the client.

Server vs Client Components

Feature Server Component Client Component
Render location Server only Server + client
JavaScript shipped None Component code
Can use hooks No Yes
Can use browser APIs No Yes
Can fetch data Yes (async/await) Yes (useEffect, etc.)
Can access server Yes (directly) No (needs API)
Can import server-only Yes No

Default behavior

In Next.js App Router, all components are Server Components by default:

// app/page.tsx - Server Component (default)
export default async function Page() {
  const data = await fetchData();
  return <div>{data.title}</div>;
}

No directive needed. The component runs on the server.

Creating Client Components

Add "use client" at the top of a file:

'use client';

import { useState } from 'react';

export function Counter() {
  const [count, setCount] = useState(0);

  return (
    <button onClick={() => setCount(count + 1)}>
      Count: {count}
    </button>
  );
}

The directive marks the file—and everything it imports—as client code.

When to use Server Components

Use Server Components for:

Use case Why
Data fetching Direct database/API access without client exposure
Heavy computations Process on server, send result
Large dependencies Keep markdown parsers, image processors off client
Sensitive logic API keys, business rules stay on server
Static content No JS overhead for non-interactive content

When to use Client Components

Use Client Components for:

Use case Why
Interactivity onClick, onChange, form handling
React hooks useState, useEffect, useContext
Browser APIs window, localStorage, navigator
Third-party UI Libraries that use hooks internally
Real-time updates WebSocket subscriptions

Composition patterns

Pattern 1: Server parent, Client child

The most common pattern—Server Component fetches data, Client Component handles interaction:

// app/products/page.tsx (Server Component)
import { ProductFilter } from './ProductFilter';

export default async function ProductsPage() {
  const categories = await getCategories();
  const products = await getProducts();

  return (
    <div>
      {/* Client Component receives server data as props */}
      <ProductFilter categories={categories} />
      <ProductList products={products} />
    </div>
  );
}

// app/products/ProductFilter.tsx (Client Component)
'use client';

import { useState } from 'react';

export function ProductFilter({ categories }) {
  const [selected, setSelected] = useState(null);
  // Interactive filter UI
}

Pattern 2: Children as Server Components

Pass Server Component output as children to Client Components:

// Server Component
export default async function Page() {
  return (
    <ClientWrapper>
      <ServerContent /> {/* Rendered on server, passed as children */}
    </ClientWrapper>
  );
}

// Client Component
'use client';

export function ClientWrapper({ children }) {
  const [isOpen, setIsOpen] = useState(true);

  return (
    <div>
      <button onClick={() => setIsOpen(!isOpen)}>Toggle</button>
      {isOpen && children} {/* Server-rendered content */}
    </div>
  );
}

The Server Component renders before being passed. It doesn't become a Client Component.

Pattern 3: Slots for flexibility

Use props to pass multiple Server Component trees:

// Server Component
export default async function DashboardPage() {
  return (
    <DashboardLayout
      sidebar={<Sidebar />}      {/* Server Component */}
      header={<Header />}        {/* Server Component */}
      main={<DashboardContent />} {/* Server Component */}
    />
  );
}

// Client Component
'use client';

export function DashboardLayout({ sidebar, header, main }) {
  const [sidebarOpen, setSidebarOpen] = useState(true);

  return (
    <div className="flex">
      {sidebarOpen && sidebar}
      <div className="flex-1">
        {header}
        {main}
      </div>
    </div>
  );
}

Data fetching

Server Components can fetch data directly:

// Direct fetch
async function Posts() {
  const posts = await fetch('https://api.example.com/posts').then(r => r.json());
  return <PostList posts={posts} />;
}

// Database query
async function Users() {
  const users = await db.select().from(usersTable);
  return <UserList users={users} />;
}

// File system access
async function Docs() {
  const content = await fs.readFile('./docs/intro.md', 'utf-8');
  return <MarkdownRenderer content={content} />;
}

No useEffect, no loading states in the component—Suspense handles loading.

Server-only code

Mark code that should never run on client:

import 'server-only';

export async function getSecretData() {
  const apiKey = process.env.SECRET_API_KEY;
  // This throws at build time if imported in Client Component
}

Import server-only in modules that use secrets or server APIs.

Streaming and Suspense

Server Components can stream with Suspense:

import { Suspense } from 'react';

export default function Page() {
  return (
    <div>
      <Header /> {/* Sends immediately */}

      <Suspense fallback={<Skeleton />}>
        <SlowDataComponent /> {/* Streams when ready */}
      </Suspense>

      <Footer /> {/* Sends immediately */}
    </div>
  );
}

The page structure sends immediately. Slow components stream in.

Common mistakes

Mistake Fix
"use client" on everything Only add where needed
Hooks in Server Components Move to Client Component
Importing Client in Server Works fine, but creates boundary
Large Client Components Split into smaller Client + Server parts
Prop drilling to avoid Client Use composition patterns instead

Bundle size impact

Server Components keep dependencies off the client:

// Server Component - these don't ship to client
import { marked } from 'marked';      // ~50KB
import { highlight } from 'prism';    // ~100KB
import { processImage } from 'sharp'; // Large native module

async function BlogPost({ slug }) {
  const post = await getPost(slug);
  const html = marked(post.content);
  return <article dangerouslySetInnerHTML={{ __html: html }} />;
}

The client receives only the rendered HTML.

Caching and revalidation

Server Component data can be cached:

// Cached indefinitely
const data = await fetch(url);

// Revalidate every hour
const data = await fetch(url, { next: { revalidate: 3600 } });

// No caching
const data = await fetch(url, { cache: 'no-store' });

Use revalidatePath() or revalidateTag() for on-demand revalidation.

Debugging

Check where components run:

// Add to any component
console.log('Running on:', typeof window === 'undefined' ? 'server' : 'client');

In Next.js dev tools, Server Components show in the component tree without the client bundle indicator.

Summary

Server Components render on the server and ship no JavaScript. They can access databases, file systems, and server APIs directly. Use them by default for data fetching and non-interactive UI. Add "use client" only for components that need hooks or browser APIs. Compose by passing Server Component output as children or props to Client Components. The result is smaller bundles and faster page loads.

Frequently Asked Questions

React Server Components are components that render only on the server. They don't ship JavaScript to the client, can access server resources directly, and send rendered HTML or RSC payload to the browser.

Yes. Server Components and their dependencies aren't included in the client bundle. Heavy libraries for data processing, markdown rendering, or database access stay on the server.

No. Server Components can't use hooks like useState, useEffect, or useContext. They run once on the server and don't have client-side state. Use Client Components for interactivity.

Keep Server Components as parents and pass their output to Client Components as children props. Add 'use client' only to components that need interactivity. The boundary is explicit.

The RSC payload is a serialized format that represents the rendered Server Component tree. It's sent to the client where React reconstructs the UI without re-rendering the server parts.

Related Posts