Web Development

React Server vs Client Components: When to Use What

The Debuggers Engineering Team
10 min read

TL;DR

  • Components are Server Components by default in Next.js App Router. Add 'use client' only when you need browser APIs, hooks, or event handlers
  • Server Components add zero bytes to the JavaScript bundle, reducing page weight and improving LCP
  • The composition pattern (server component wrapping client component, passing data as props) is the key architecture skill
  • Only data that is serialisable (JSON-compatible) can cross the server-to-client boundary

Table of Contents

React component architecture diagram with server and client layers

The Mental Model Shift

Before React Server Components, every React component was a client component. Your entire component tree shipped as JavaScript to the browser, hydrated, and became interactive. This was simple to understand but expensive in bundle size.

With Server Components in Next.js App Router, the default flipped. Every component is a Server Component unless you explicitly opt out with 'use client'. Server Components run on the server during rendering, produce HTML, and send zero JavaScript to the browser.

Think of it as two zones:

Server zone: Components that fetch data, access databases, read files, and produce static HTML. They never re-render on the client. They cannot use useState, useEffect, or any browser API.

Client zone: Components that handle user interaction, manage local state, use browser APIs, and re-render in the browser. They ship JavaScript and hydrate like traditional React components.

The 'use client' directive is the boundary between these zones. Everything above it (in the import tree) stays on the server. Everything below it (including its children) runs on the client.

What Server Components Can and Cannot Do

Can do:

  • async/await directly in the component body (fetch data inline)
  • Access databases, file systems, and server-only APIs
  • Use server-only packages (Node.js built-ins, database drivers)
  • Render HTML without adding to the client JavaScript bundle
  • Pass React elements (JSX) as props to client components

Cannot do:

  • Use React hooks (useState, useEffect, useRef, useContext)
  • Attach event handlers (onClick, onChange, onSubmit)
  • Use browser APIs (window, document, localStorage)
  • Use third-party libraries that depend on browser APIs
  • Re-render in response to user interaction
// This is a Server Component (default in App Router)
import { db } from '@/lib/database';

export default async function UserList() {
  const users = await db.user.findMany(); // Direct database access

  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name} - {user.email}</li>
      ))}
    </ul>
  );
}

Server-side rendering process showing data flow from database to HTML

What Client Components Can and Cannot Do

Can do:

  • Use all React hooks
  • Handle user events (clicks, inputs, gestures)
  • Access browser APIs
  • Manage local UI state
  • Use third-party UI libraries (date pickers, rich text editors, charts)

Cannot do:

  • Access the file system or databases directly (security risk)
  • Use server-only packages
  • Be async (no async function Component())
  • Import Server Components (but can receive them as children props)
'use client';

import { useState } from 'react';

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

  return (
    <input
      value={query}
      onChange={(e) => {
        setQuery(e.target.value);
        onSearch(e.target.value);
      }}
      placeholder="Search users..."
    />
  );
}

The Composition Pattern

The key skill with Server Components is the composition pattern: a Server Component that wraps Client Components and passes server-fetched data as props.

// page.tsx (Server Component)
import { db } from '@/lib/database';
import { UserTable } from './user-table'; // Client Component

export default async function UsersPage() {
  const users = await db.user.findMany({
    select: { id: true, name: true, email: true, role: true }
  });

  // Server fetches data, Client renders interactive table
  return <UserTable initialUsers={users} />;
}
// user-table.tsx (Client Component)
'use client';

import { useState } from 'react';

export function UserTable({ initialUsers }: { initialUsers: User[] }) {
  const [sortField, setSortField] = useState<keyof User>('name');
  const [filterRole, setFilterRole] = useState<string>('all');

  const filtered = initialUsers
    .filter(u => filterRole === 'all' || u.role === filterRole)
    .sort((a, b) => String(a[sortField]).localeCompare(String(b[sortField])));

  return (
    <div>
      <select value={filterRole} onChange={e => setFilterRole(e.target.value)}>
        <option value="all">All Roles</option>
        <option value="admin">Admin</option>
        <option value="user">User</option>
      </select>
      <table>
        <thead>
          <tr>
            <th onClick={() => setSortField('name')}>Name</th>
            <th onClick={() => setSortField('email')}>Email</th>
            <th>Role</th>
          </tr>
        </thead>
        <tbody>
          {filtered.map(user => (
            <tr key={user.id}>
              <td>{user.name}</td>
              <td>{user.email}</td>
              <td>{user.role}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

The database query runs on the server, adds zero JavaScript to the bundle, and exposes no database credentials to the client. The interactive sorting and filtering run on the client with the pre-fetched data.

Performance Implications

Bundle size reduction: A Server Component with 500 lines of code and 10 imported utility functions adds 0 bytes to the client JavaScript bundle. The same component as a client component might add 15-50KB depending on its dependencies.

LCP improvement: Server Components produce HTML immediately, which the browser can paint without waiting for JavaScript to download, parse, and execute. This directly improves Largest Contentful Paint.

TTFB consideration: Server Components shift computation to the server, which can increase Time to First Byte if your server is slow or far from the user. Use streaming (with Suspense) to start sending HTML before all data is ready.

Hydration cost: Client Components must hydrate (attach event listeners and reconcile the DOM). More client components means more hydration work, which can block interactivity (measured by INP). Fewer client components means faster interactivity.

Common Mistakes

Marking too many components as client

// BAD: The entire page is a client component because of one useState
'use client';

export default function ProductPage({ params }) {
  const [quantity, setQuantity] = useState(1);
  // ...200 lines of product display that doesn't need client JS
}

// GOOD: Only the interactive part is a client component
export default async function ProductPage({ params }) {
  const product = await fetchProduct(params.id);
  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      <QuantitySelector /> {/* Only this needs 'use client' */}
    </div>
  );
}

Importing server-only code in client components

Client components cannot import modules that use Node.js APIs. The server-only package helps enforce this:

// lib/database.ts
import 'server-only'; // Throws build error if imported from client component
import { PrismaClient } from '@prisma/client';
export const db = new PrismaClient();

Using context incorrectly

useContext only works in client components. If you need to share data across server components, pass it as props or use the cookies() / headers() functions.

The Serialisation Boundary

Data passed from Server Components to Client Components must be serialisable - it passes through the React Server Components wire format, which supports JSON-compatible types.

Serialisable: strings, numbers, booleans, null, arrays, plain objects, Date, Map, Set, TypedArrays, ReadableStream, Promise.

Not serialisable: functions, classes with methods, React elements from server components (they are already serialised), symbols, Error objects.

// This works
<ClientComponent data={{ name: "test", createdAt: new Date() }} />

// This does NOT work
<ClientComponent 
  onSubmit={() => console.log('clicked')} // Function cannot cross boundary
  dbClient={prisma} // Class with methods cannot cross boundary
/>

Component boundary diagram showing data flow between server and client

Architecture Example

Here is a complete page architecture using both server and client components:

page.tsx          (Server) - fetches data, renders layout
  Header.tsx      (Server) - static navigation, no interactivity
    MobileMenu.tsx  (Client) - hamburger menu toggle needs useState
  ProductGrid.tsx (Server) - maps over products, pure rendering
    ProductCard.tsx (Server) - displays product info
      AddToCart.tsx  (Client) - button with onClick + cart state
  Sidebar.tsx     (Server) - category list, static
    PriceFilter.tsx (Client) - range slider needs useState
  Footer.tsx      (Server) - static footer content

Five out of eight components are Server Components that add zero JavaScript to the bundle. Only the three interactive components ship client-side code.

When building your Next.js API routes that serve data to these components, test your API endpoints with our free API tester and validate the JSON response structure with our formatter.

For details on upgrading to the latest Next.js version, see our Next.js 15 App Router migration guide.

The Debuggers builds Next.js applications using modern React patterns, including Server Components architecture for optimal performance and SEO.

Server Actions vs Server Components

Server Actions ('use server') and Server Components are different concepts that often cause confusion:

Server Components: Run on the server during rendering. They produce HTML. They are for reading data.

Server Actions: Functions that run on the server in response to user actions (form submissions, button clicks). They are for writing data (mutations).

// Server Action (for mutations)
'use server';
export async function createUser(formData: FormData) {
  await db.user.create({ data: { name: formData.get('name') as string } });
  revalidatePath('/users');
}

// Server Component (for reads)
export default async function UsersPage() {
  const users = await db.user.findMany();
  return <UserList users={users} createAction={createUser} />;
}

Frequently Asked Questions

Can a Client Component render a Server Component?

Not directly through import. A Client Component cannot import and render a Server Component because the client bundle does not include server-side code. However, a Client Component can render Server Components that are passed as children or other React element props. This is the "donut pattern" where a client component wraps a hole that server components fill from above.

Do Server Components replace API routes?

For data fetching within your own application, yes. Server Components can query databases directly without needing an API endpoint as an intermediary. API routes are still needed for external consumers (mobile apps, third-party integrations) and for webhooks. The pattern is: Server Components for your own Next.js pages, API routes for external access.

How do Server Components affect SEO?

Server Components improve SEO significantly. They render HTML on the server, which search engines receive immediately without executing JavaScript. This means your content is fully indexable with correct meta tags and heading structure. Client Components that render content dynamically may not be indexed by all search engines, though Google's crawler does execute JavaScript.

Should I convert my entire app to Server Components?

No. Convert components that are data-fetching wrappers, static content displays, and layout shells. Keep components that handle user interaction, manage form state, or use browser APIs as client components. The goal is not zero client components but minimal, well-scoped client components that contain only the interactive logic they need.


Building with React Server Components?

Validate your API response structures with our free JSON Formatter before designing your component data flow. Ensure your serialisable props match the data your server fetches.

Need help architecting a Next.js application? The Debuggers provides React and Next.js consulting for teams adopting modern patterns.

Need Help Implementing This in a Real Project?

Our team supports end-to-end development for web and mobile software, from architecture to launch.

react server components vs client componentsreact server components explaineduse client directiveRSC performancenextjs server components

Found this helpful?

Join thousands of developers using our tools to write better code, faster.