← Articles

React Server Components vs Client Components: When to Use Which

By Mark · 29 June 20260 views

React Server Components vs Client Components: When to Use Which

React Server Components (RSC) represent the most significant change to React's rendering model since hooks. Introduced with React 18 and fully embraced by Next.js 13+, RSC fundamentally changes where and how components render. Understanding the distinction between Server and Client Components is essential for building efficient Next.js applications in 2026.

The Core Distinction

Server Components render on the server, at request time (or build time for static routes). They can access databases, file systems, and environment variables directly. They never run in the browser and their code is never sent to the client.

Client Components render in the browser (and also on the server during SSR hydration). They have access to browser APIs, React state (useState), effects (useEffect), and event handlers.

In the App Router, all components are Server Components by default. You opt into Client Components by adding 'use client' at the top of the file.

What Server Components Can Do

// app/dashboard/page.tsx — Server Component by default
import { db } from '@/lib/database';

export default async function DashboardPage() {
  // Direct database access — no API layer needed
  const user = await db.query('SELECT * FROM users WHERE id = $1', [userId]);
  const stats = await fetchStats(userId);

  return (
    <main>
      <h1>Welcome, {user.name}</h1>
      <StatsPanel stats={stats} />
    </main>
  );
}

Server Components can be async — you await data directly in the component body. There is no useEffect + loading state pattern needed for initial data.

What Only Client Components Can Do

You must use 'use client' when your component:

  • Uses useState or useReducer
  • Uses useEffect or useLayoutEffect
  • Attaches event listeners (onClick, onChange, etc.)
  • Uses browser APIs (window, localStorage, navigator)
  • Uses third-party hooks that rely on the above
'use client';

import { useState } from 'react';

export function SearchBar({ onSearch }: { onSearch: (q: string) => void }) {
  const [query, setQuery] = useState('');

  return (
    <input
      value={query}
      onChange={(e) => setQuery(e.target.value)}
      onKeyDown={(e) => e.key === 'Enter' && onSearch(query)}
    />
  );
}

Composing Server and Client Components

The key rule: Server Components can import and render Client Components, but Client Components cannot import Server Components (they can receive them as children props, though).

// Server Component (page.tsx)
import { ProductList } from './ProductList'; // Server Component
import { AddToCartButton } from './AddToCartButton'; // Client Component

export default async function ProductPage({ params }) {
  const product = await db.getProduct(params.id);
  return (
    <div>
      <ProductList product={product} />
      <AddToCartButton productId={product.id} />
    </div>
  );
}

Push 'use client' as deep in the component tree as possible. If only a button needs interactivity, make only the button a Client Component — not the entire page.

The Bundle Size Impact

Server Component code is never included in the JavaScript bundle sent to the browser. A heavy library used only in a Server Component (a Markdown parser, a database driver, a large data transformation utility) costs zero bytes on the client. This is one of the most significant performance benefits of RSC.

Data Fetching Patterns

Before RSC (pages router):

export async function getServerSideProps() {
  const data = await fetchData();
  return { props: { data } };
}

With RSC:

export default async function Page() {
  const data = await fetchData(); // Directly in the component
  return <DataView data={data} />;
}

RSC also enables parallel data fetching naturally:

export default async function Page() {
  // These run in parallel
  const [user, posts, stats] = await Promise.all([
    getUser(),
    getPosts(),
    getStats(),
  ]);
  return <Dashboard user={user} posts={posts} stats={stats} />;
}

Common Mistakes

Marking too many components as 'use client': Developers sometimes put 'use client' at the top of their root layout because they need useState somewhere. This makes every component below it a Client Component, eliminating all RSC benefits.

Importing server-only code in Client Components: If you accidentally import a database module in a Client Component, Next.js will try to bundle it for the browser. Use the server-only package to prevent this:

import 'server-only'; // Throws at build time if imported in a Client Component

Conclusion

The decision rule is simple: use Server Components for data fetching and non-interactive rendering; use Client Components for anything that requires browser APIs, state, or event handlers. Default to Server Components, and add 'use client' only where the browser is genuinely needed. This approach maximizes bundle size savings and simplifies data fetching while keeping interactivity where it belongs.

Sign in to like, dislike, or report.

Comments

No comments yet. Be the first!

Sign in to leave a comment.