Skip to main content
TWYTech World by Yashrajsinh

React Server vs Client

Y
Yashrajsinh
··10 min read·Intermediate

React Server vs Client

React Server Components represent the most significant architectural shift in React since hooks. They introduce a new mental model where components can execute exclusively on the server, never shipping their JavaScript to the browser. This means you can query databases, access the filesystem, and call internal APIs directly inside your components without exposing secrets or bloating the client bundle. Combined with Client Components that handle interactivity, this dual-component model gives you the best of both worlds: fast initial loads with rich user experiences.

Understanding when to use Server Components versus Client Components is essential for building performant modern React applications. The wrong choice leads to either unnecessarily large bundles that slow down page loads or missing interactivity that frustrates users. This guide walks through the architecture, explains the rendering model, demonstrates practical patterns, and helps you make the right decision for every component in your application. A solid foundation in React hooks and patterns is assumed, as Client Components rely heavily on hooks for state and effects.

What You Will Learn

By working through this guide, you will gain a comprehensive understanding of the Server Components architecture and how to apply it effectively in production applications:

  • How React Server Components execute on the server and stream their output as a serialized tree to the client without shipping component JavaScript
  • The role of the "use client" directive as a boundary marker that tells the bundler where the server-to-client transition happens
  • When to choose Server Components for data fetching, static rendering, and reducing bundle size versus Client Components for interactivity, browser APIs, and state management
  • How Server Components and Client Components compose together through props, children, and the component tree hierarchy
  • Practical patterns for fetching data in Server Components and passing it to interactive Client Components without waterfalls
  • How streaming and Suspense work with Server Components to deliver progressive page loads
  • Performance implications of each component type and how the architecture affects Core Web Vitals metrics
  • Common mistakes developers make when adopting Server Components and how to avoid them

Each section builds progressively, but experienced developers can jump directly to the patterns and best practices sections.

Prerequisites

Before diving into Server Components, ensure you have the following knowledge and environment ready:

  • Strong understanding of React component composition, props, and the component lifecycle as covered in the React hooks and patterns guide
  • Familiarity with async/await patterns and Promises in JavaScript since Server Components can be async functions
  • A working Next.js 14 or later project with the App Router enabled, as Server Components are the default in the App Router
  • Understanding of the difference between server-side rendering and client-side rendering at a conceptual level
  • Basic knowledge of HTTP request/response cycles and how web servers deliver content to browsers
  • Experience with JavaScript fundamentals including modules, destructuring, and template literals

If you have built at least one Next.js App Router application or experimented with React Server Components in a framework, you are ready for this guide.

Concept Overview

The traditional React model sends all component JavaScript to the browser. Every component, whether it displays static text or manages complex interactive state, contributes to the bundle that users must download, parse, and execute before the page becomes interactive. Server Components change this by splitting the component tree into two environments: components that run only on the server and components that run on the client.

Server Components execute during the request on the server. They can directly access databases, read files, call internal microservices, and use server-only libraries. Their output is a serialized React tree, essentially a description of what to render, that gets streamed to the client. The client receives this description and renders it into the DOM without ever needing the component's source code. This means Server Components contribute zero bytes to the client JavaScript bundle.

Client Components are the components you already know. They run in the browser, can use hooks like useState and useEffect, respond to user events, and access browser APIs like localStorage and window. You mark a component as a Client Component by adding the "use client" directive at the top of its file. Everything below that directive, including its imports, becomes part of the client bundle.

The boundary between server and client is not a hard wall but a composition point. Server Components can render Client Components as children, passing serializable props down. Client Components cannot import or render Server Components directly, but they can accept Server Components as children or other React node props. This composition model is the key to building applications that are both fast and interactive.

Step-by-Step Explanation

This section walks through the core decision-making steps for choosing between React Server Components and Client Components. Each step builds on the previous one, providing a clear path from understanding the rendering model through data fetching patterns to composing hybrid architectures that maximize performance.

The Server Component Rendering Model

When a request arrives at your application, the server begins rendering the component tree from the root. Every component without a "use client" directive is a Server Component by default. The server executes these components, resolves their async operations, and produces a React Server Component Payload, a compact binary format that describes the rendered tree.

// app/products/page.tsx - This is a Server Component by default
import { db } from "@/lib/database";
import { ProductCard } from "@/components/ProductCard";
import { AddToCartButton } from "@/components/AddToCartButton";
 
interface Product {
  id: string;
  name: string;
  price: number;
  description: string;
  imageUrl: string;
}
 
export default async function ProductsPage() {
  // Direct database access - no API route needed
  const products: Product[] = await db.query(
    "SELECT id, name, price, description, image_url FROM products WHERE active = true ORDER BY created_at DESC"
  );
 
  return (
    <main>
      <h1>Our Products</h1>
      <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
        {products.map((product) => (
          <ProductCard key={product.id} product={product}>
            {/* Client Component nested inside Server Component */}
            <AddToCartButton productId={product.id} price={product.price} />
          </ProductCard>
        ))}
      </div>
    </main>
  );
}
 
// components/ProductCard.tsx - Server Component (no "use client")
interface ProductCardProps {
  product: Product;
  children: React.ReactNode;
}
 
export function ProductCard({ product, children }: ProductCardProps) {
  return (
    <article className="border rounded-lg p-4">
      <img src={product.imageUrl} alt={product.name} width={300} height={200} />
      <h2>{product.name}</h2>
      <p>{product.description}</p>
      <p className="font-bold">${product.price.toFixed(2)}</p>
      {children}
    </article>
  );
}

In this example, ProductsPage and ProductCard are Server Components. They execute on the server, query the database directly, and produce HTML that streams to the client. The AddToCartButton is a Client Component that handles the interactive add-to-cart behavior. The server renders the product data and leaves a placeholder for the client component, which hydrates on the browser.

The Client Boundary Directive

The "use client" directive marks the entry point into client-side code. It does not mean the component only renders on the client. Client Components still get server-rendered for the initial HTML, but their JavaScript ships to the browser for hydration and interactivity. The directive affects the entire module and all its imports.

"use client";
 
import { useState, useTransition } from "react";
 
interface AddToCartButtonProps {
  productId: string;
  price: number;
}
 
export function AddToCartButton({ productId, price }: AddToCartButtonProps) {
  const [quantity, setQuantity] = useState(1);
  const [isPending, startTransition] = useTransition();
  const [added, setAdded] = useState(false);
 
  async function handleAddToCart() {
    startTransition(async () => {
      const response = await fetch("/api/cart", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ productId, quantity }),
      });
 
      if (response.ok) {
        setAdded(true);
        setTimeout(() => setAdded(false), 2000);
      }
    });
  }
 
  return (
    <div className="flex items-center gap-2 mt-4">
      <label htmlFor={`qty-${productId}`} className="sr-only">
        Quantity
      </label>
      <input
        id={`qty-${productId}`}
        type="number"
        min={1}
        max={99}
        value={quantity}
        onChange={(e) => setQuantity(Number(e.target.value))}
        className="w-16 border rounded px-2 py-1"
        aria-label="Quantity"
      />
      <button
        onClick={handleAddToCart}
        disabled={isPending}
        className="bg-blue-600 text-white px-4 py-2 rounded disabled:opacity-50"
      >
        {isPending ? "Adding..." : added ? "Added!" : `Add to Cart - $${(price * quantity).toFixed(2)}`}
      </button>
    </div>
  );
}

This Client Component uses useState for local state and useTransition for non-blocking updates. It needs to run in the browser because it responds to user clicks, manages form input state, and calls a browser fetch API. The "use client" directive ensures its code ships to the browser bundle.

Composition Patterns Between Server and Client

The most powerful aspect of the Server Components architecture is how the two types compose together. Server Components can pass any serializable data as props to Client Components. They can also pass pre-rendered React elements as children, which means you can wrap interactive client boundaries around server-rendered content.

// app/dashboard/page.tsx - Server Component
import { getUser, getAnalytics } from "@/lib/data";
import { DashboardShell } from "@/components/DashboardShell";
import { AnalyticsChart } from "@/components/AnalyticsChart";
import { RecentActivity } from "@/components/RecentActivity";
 
export default async function DashboardPage() {
  const user = await getUser();
  const analytics = await getAnalytics(user.id);
 
  return (
    <DashboardShell userName={user.name}>
      {/* Server-rendered content passed as children to a Client Component */}
      <section>
        <h2>Your Analytics</h2>
        <AnalyticsChart data={analytics.chartData} />
      </section>
      <section>
        <h2>Recent Activity</h2>
        <RecentActivity activities={analytics.recentItems} />
      </section>
    </DashboardShell>
  );
}
"use client";
 
import { useState, ReactNode } from "react";
 
interface DashboardShellProps {
  userName: string;
  children: ReactNode;
}
 
export function DashboardShell({ userName, children }: DashboardShellProps) {
  const [sidebarOpen, setSidebarOpen] = useState(true);
 
  return (
    <div className="flex">
      <aside className={sidebarOpen ? "w-64" : "w-16"}>
        <button
          onClick={() => setSidebarOpen(!sidebarOpen)}
          aria-label={sidebarOpen ? "Collapse sidebar" : "Expand sidebar"}
        >
          {sidebarOpen ? "←" : "→"}
        </button>
        {sidebarOpen && <nav aria-label="Dashboard navigation">
          <p>Welcome, {userName}</p>
        </nav>}
      </aside>
      <main className="flex-1 p-6">{children}</main>
    </div>
  );
}

Here, DashboardShell is a Client Component that manages sidebar toggle state. But its children prop contains server-rendered content from DashboardPage. The analytics data was fetched on the server and the sections were rendered there. The Client Component simply places them in the DOM without needing to know how they were produced. This pattern keeps the interactive shell lightweight while the heavy data fetching stays on the server.

Data Fetching Patterns

Server Components eliminate the need for client-side data fetching libraries in many cases. Instead of useEffect with loading states, you fetch data directly in the component. Combined with streaming and Suspense, this delivers content progressively without blocking the entire page.

// app/blog/[slug]/page.tsx - Server Component with parallel data fetching
import { Suspense } from "react";
import { getPost, getComments, getRelatedPosts } from "@/lib/blog";
import { CommentSection } from "@/components/CommentSection";
 
interface PageProps {
  params: { slug: string };
}
 
export default async function BlogPost({ params }: PageProps) {
  // Start both fetches in parallel
  const postPromise = getPost(params.slug);
  const relatedPromise = getRelatedPosts(params.slug);
 
  const post = await postPromise;
 
  return (
    <article>
      <h1>{post.title}</h1>
      <time dateTime={post.publishedAt}>{post.publishedAt}</time>
      <div dangerouslySetInnerHTML={{ __html: post.htmlContent }} />
 
      {/* Stream comments independently - don't block the article */}
      <Suspense fallback={<p>Loading comments...</p>}>
        <Comments slug={params.slug} />
      </Suspense>
 
      {/* Stream related posts independently */}
      <Suspense fallback={<p>Loading related posts...</p>}>
        <RelatedPosts promise={relatedPromise} />
      </Suspense>
    </article>
  );
}
 
// Async Server Component that streams when ready
async function Comments({ slug }: { slug: string }) {
  const comments = await getComments(slug);
  return <CommentSection initialComments={comments} slug={slug} />;
}
 
async function RelatedPosts({ promise }: { promise: Promise<Post[]> }) {
  const posts = await promise;
  return (
    <aside>
      <h2>Related Posts</h2>
      <ul>
        {posts.map((post) => (
          <li key={post.slug}>
            <a href={`/blog/${post.slug}`}>{post.title}</a>
          </li>
        ))}
      </ul>
    </aside>
  );
}

The article content renders immediately while comments and related posts stream in as they become available. Each Suspense boundary acts as an independent loading unit. The user sees the main content instantly and the supplementary content appears progressively. This pattern dramatically improves perceived performance compared to client-side fetching where the entire page shows a spinner until all data arrives.

Real-World Use Cases

Server Components and Client Components work together across many application types and scenarios in production:

  • E-commerce product pages where Server Components fetch product data, inventory status, and pricing from the database while Client Components handle the shopping cart, quantity selectors, and wishlist buttons that require user interaction
  • Content management dashboards where Server Components render the article list with metadata fetched from the CMS while Client Components provide inline editing, drag-and-drop reordering, and real-time collaboration indicators
  • Analytics platforms where Server Components aggregate and transform large datasets on the server, reducing the data sent to the client, while Client Components render interactive charts with zoom, pan, and tooltip behaviors
  • Authentication flows where Server Components check session validity and render protected content without exposing auth tokens to the client while Client Components handle login forms, password visibility toggles, and OAuth redirect flows
  • Multi-tenant SaaS applications where Server Components resolve the current tenant from the request headers and fetch tenant-specific configuration while Client Components provide the interactive workspace with real-time updates

These patterns apply whether you are building with Next.js, Remix, or any framework that supports the React Server Components protocol. The deployment strategy, whether using Docker containers or serverless functions, determines where the server execution happens but does not change the component architecture.

Best Practices

Following these practices ensures you get the full performance and developer experience benefits of the Server Components architecture:

  • Default to Server Components for everything and only add "use client" when a component genuinely needs interactivity, browser APIs, or React hooks that require client execution
  • Push the client boundary as far down the tree as possible so that the smallest possible subtree ships JavaScript to the browser, keeping the majority of your application server-rendered
  • Never pass non-serializable values like functions, class instances, or Symbols as props from Server Components to Client Components since the serialization boundary cannot handle them
  • Use the children pattern to pass server-rendered content through Client Components rather than trying to import Server Components inside Client Components, which is not allowed
  • Fetch data in Server Components close to where it is used rather than fetching everything at the page level and passing it down through many layers of props
  • Wrap independent data-fetching sections in Suspense boundaries to enable streaming and prevent slow queries from blocking the entire page render
  • Keep Client Components focused on interactivity and presentation logic, delegating data fetching and business logic to Server Components or Server Actions
  • Use environment variables prefixed with the framework's public prefix only in Client Components, keeping sensitive keys in server-only code where they cannot leak to the browser bundle
  • Leverage the Git branching strategies to experiment with Server Component refactoring on feature branches before merging to main

Common Mistakes

These are the most frequent errors developers encounter when working with Server Components and Client Components:

  • Adding "use client" to every component out of habit or confusion, which defeats the purpose of Server Components and results in the same large bundles as traditional React applications
  • Trying to use useState, useEffect, or other hooks in Server Components, which causes a build error because hooks require the client runtime to function
  • Importing a Server Component inside a Client Component file, which silently converts it to a Client Component and includes all its dependencies in the client bundle
  • Passing functions as props from Server Components to Client Components, which fails at runtime because functions cannot be serialized across the server-client boundary
  • Fetching data in Client Components with useEffect when the same data could be fetched more efficiently in a parent Server Component and passed as props
  • Not using Suspense boundaries around async Server Components, which causes the entire page to wait for the slowest data fetch before anything renders
  • Confusing Server Components with server-side rendering, they are different concepts where SSR renders Client Components on the server for initial HTML while Server Components never ship their code to the client at all
  • Placing the "use client" directive below import statements, which has no effect since the directive must be the very first line in the file before any imports

Summary

React Server Components fundamentally change how we think about building React applications. By splitting the component tree into server-executed and client-executed parts, you get the performance benefits of server rendering with the interactivity of client-side React. Server Components handle data fetching, access server resources, and produce zero-JavaScript output. Client Components handle user interactions, manage state, and use browser APIs.

The composition model, where Server Components render Client Components as children and pass serializable props across the boundary, gives you fine-grained control over what ships to the browser. Combined with streaming and Suspense, this architecture delivers fast initial loads with progressive enhancement as content becomes available.

Mastering this architecture is essential for modern React development. Whether you are building with Next.js App Router or another framework that supports the protocol, the patterns covered here apply universally. Start by defaulting to Server Components, push client boundaries down the tree, and use Suspense for progressive loading. With these principles in place, explore React state management patterns to understand how state flows through applications that span both server and client environments, and consider how performance optimization techniques complement the Server Components architecture for maximum speed.

Intermediate10 min read

React Forms and Validation Patterns Guide

Learn how to build robust React forms with validation, error handling, accessibility, and scalable patterns for complex multi-field applications.

Intermediate12 min read

React Hooks and Component Patterns Guide

Learn React components, hooks, state management, effects, context, routing, and performance-friendly UI patterns for production applications.

Advanced11 min read

React Performance Guide

Master React rendering behavior, profiling techniques, memoization strategies, and architectural patterns for building high-performance applications.