Skip to content
C Codeloom
React

React Server Components vs Client Components

Understand the boundary between Server and Client Components in React, when to use each, and how data, state, and bundles flow between them.

·4 min read · By Codeloom
Intermediate 9 min read

What you'll learn

  • What Server and Client Components actually are
  • How the use client directive marks boundaries
  • When to choose Server over Client and vice versa
  • Passing data and props across the boundary
  • Avoiding common composition mistakes

Prerequisites

  • Comfortable with JS
  • Some React knowledge

What and Why

React Server Components (RSC) are components that run only on the server. They can read databases, access the file system, and use server-only secrets. They send a serialized tree to the client, but they ship zero JavaScript for themselves. Client Components are what you have always written: they run in the browser, can hold state, and use effects.

The split exists for two reasons. First, performance: heavy logic like markdown rendering or database queries belongs on the server, where it does not slow down the user. Second, bundle size: by default, more of your app runs server-side, and only interactive islands ship JS.

Mental Model

Picture a tree of components. The root and many leaves are Server Components. When you need interactivity, you draw a boundary using 'use client' at the top of a file, and from that point down the subtree becomes Client. Children of a Client Component can still be Server Components if they were passed as props or children.

<Page>            (Server)
<Header/>       (Server)
<Sidebar/>      (Server)
<SearchBox/>    (Client)  -- use client
  <Spinner/>    (Client)
<ArticleList/>  (Server)
  <Article/>    (Server)
Server / Client boundary tree

Hands-on Example

Imagine a blog page. The page itself fetches posts from a database. A search box at the top is interactive.

// app/blog/page.tsx (Server Component)
import { db } from '@/lib/db';
import SearchBox from './SearchBox';
import ArticleList from './ArticleList';

export default async function BlogPage() {
  const posts = await db.posts.findMany();
  return (
    <main>
      <SearchBox />
      <ArticleList posts={posts} />
    </main>
  );
}
// app/blog/SearchBox.tsx
'use client';
import { useState } from 'react';

export default function SearchBox() {
  const [q, setQ] = useState('');
  return (
    <input
      value={q}
      onChange={(e) => setQ(e.target.value)}
      placeholder="Search"
    />
  );
}

The page does a real database call without shipping the database client to the browser. Only SearchBox ships JS. This pattern keeps your client bundle small, and you do not write a single API route for the initial render.

Browser  ->  GET /blog
Server   ->  run page (db query)
Server   ->  stream RSC payload + island bundles
Browser  <-  hydrate SearchBox only
Data flow on first request

Common Pitfalls

Putting 'use client' at the root of your app. This defeats the whole point. Push the boundary as far down the tree as possible.

Trying to import server-only code from a client file. Even an unused import in a Client Component will be bundled. Use the server-only package to fail fast, or move the call to a Server Action.

Passing non-serializable props across the boundary. Server-to-Client props must be JSON-safe: no functions, no class instances, no Dates with methods you need. If you need behavior, pass data and rebuild on the client.

Forgetting that hooks like useState, useEffect, and useContext only work in Client Components. The error message is clear, but the fix sometimes is not. Often you can split the file so only the interactive part is 'use client'.

Wrapping Server Components inside Client Components and expecting them to remain server-rendered. Once a Client Component decides what to render, it can only render Client Components. To compose a Server Component inside, pass it as children.

Best Practices

Start every component as a Server Component. Add 'use client' only when you genuinely need state, effects, browser APIs, or event handlers.

Co-locate data fetching with the component that needs it. RSC removes the awkward dance of fetching in a parent and prop-drilling down.

Pass Server Components as children to Client Components when you need a layout-like wrapper. For example, a Tabs client wrapper can render server-fetched panels.

Use Server Actions for mutations. They let you submit forms back to the server without writing API routes, and they reuse the same authentication and serialization plumbing.

Centralize sensitive code in a lib/server/ folder and mark it server-only. The bundler will refuse to include it on the client, catching mistakes early.

Measure with the bundle analyzer. If a Client Component is growing, ask whether parts of it could be moved up the tree into Server Components.

Wrap-up

Server Components let you do real work close to the data without paying client-side cost. Client Components handle interactivity. The split is not about old versus new but about where each piece runs best. Default to Server, push 'use client' to the leaves, and compose by passing Server subtrees as children. Done well, your pages feel fast because they ship less code and fetch data in fewer round trips.