Server Components vs Client Components in Next.js: The Complete Guide

Master the mental model for React Server Components in Next.js. Covers when to use each, data fetching patterns, composition strategies, and common...

Y
Yash Pritwani
13 min read

The Mental Model

In Next.js App Router, all components are Server Components by default. They render on the server, send HTML to the client, and never ship JavaScript to the browser. Client Components opt into interactivity with the "use client" directive.

docker-compose.yml123456789version: "3.8"services: web: image: nginx:alpine ports: - "80:80" volumes: - ./html:/usr/share/nginx

A well-structured configuration file is the foundation of reproducible infrastructure.

Think of it this way:

  • Server Components: HTML templates that can access databases, file systems, and APIs directly
  • Client Components: Interactive widgets that run in the browser

When to Use Server Components

Use Server Components (the default) when you:

  • Fetch data from a database or API
  • Access backend resources (file system, environment variables)
  • Keep sensitive logic on the server (API keys, business rules)
  • Render large dependencies that should not ship to the client
  • Display static or rarely changing content
// app/blog/page.tsx — Server Component (default, no directive)
import { db } from "@/lib/database";

export default async function BlogPage() {
  // This runs on the server — never exposed to the client
  const posts = await db.query("SELECT * FROM posts WHERE published = true ORDER BY created_at DESC");

  return (
    <div>
      <h1>Blog</h1>
      {posts.map((post) => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.excerpt}</p>
          <time>{new Date(post.created_at).toLocaleDateString()}</time>
        </article>
      ))}
    </div>
  );
}

Zero JavaScript shipped to the client. The database query runs on the server.

Get more insights on Tutorials

Join 2,000+ engineers who get our weekly deep-dives. No spam, unsubscribe anytime.

When to Use Client Components

Use Client Components when you need:

  • Event handlers (onClick, onChange, onSubmit)
  • State (useState, useReducer)
  • Effects (useEffect)
  • Browser APIs (window, localStorage, navigator)
  • Custom hooks that use state or effects
  • Third-party libraries that use state/effects
// components/SearchBar.tsx — Client Component
"use client";

import { useState, useCallback } from "react";
import { useRouter, useSearchParams } from "next/navigation";

export function SearchBar() {
  const router = useRouter();
  const searchParams = useSearchParams();
  const [query, setQuery] = useState(searchParams.get("q") || "");

  const handleSearch = useCallback(() => {
    const params = new URLSearchParams();
    if (query) params.set("q", query);
    router.push("/search?" + params.toString());
  }, [query, router]);

  return (
    <div>
      <input
        type="text"
        value={query}
         => setQuery(e.target.value)}
         => e.key === "Enter" && handleSearch()}
        placeholder="Search..."
      />
      <button 
    </div>
  );
}

The Composition Pattern

The most powerful pattern is composing Server Components inside Client Components using the children prop:

// components/Tabs.tsx — Client Component (interactive wrapper)
"use client";

import { useState, ReactNode } from "react";

interface TabsProps {
  tabs: { label content: ReactNode }[];
}

export function Tabs({ tabs }: TabsProps) {
  const [activeTab, setActiveTab] = useState(0);

  return (
    <div>
      <div role="tablist">
        {tabs.map((tab, i) => (
          <button
            key={i}
            role="tab"
             => setActiveTab(i)}
            aria-selected={i === activeTab}
          >
            {tab.label}
          </button>
        ))}
      </div>
      <div role="tabpanel">{tabs[activeTab].content}</div>
    </div>
  );
}
// app/dashboard/page.tsx — Server Component passing server-rendered content to client tabs
import { Tabs } from "@/components/Tabs";
import { db } from "@/lib/database";

export default async function DashboardPage() {
  const stats = await db.query("SELECT * FROM dashboard_stats");
  const activity = await db.query("SELECT * FROM recent_activity LIMIT 20");

  return (
    <Tabs
      tabs={[
        {
          label: "Overview",
          content: (
            <div>
              <h2>Statistics</h2>
              <p>Total users: {stats.totalUsers}</p>
              <p>Revenue: {stats.revenue}</p>
            </div>
          ),
        },
        {
          label: "Activity",
          content: (
            <ul>
              {activity.map((item) => (
                <li key={item.id}>{item.description}</li>
              ))}
            </ul>
          ),
        },
      ]}
    />
  );
}

The Tabs component is interactive (client), but the tab content is server-rendered. No database queries ship to the client.

Data Fetching Patterns

Terminal$docker compose up -d[+] Running 5/5Network app_default CreatedContainer web StartedContainer api StartedContainer db Started$

Docker Compose brings up your entire stack with a single command.

Server Component Fetch

// Direct database access (server only)
async function getUser(id) {
  return db.user.findUnique({ where: { id } });
}

// Fetch API with caching
async function getPosts() {
  const res = await fetch("https://api.example.com/posts", {
    next: { revalidate: 3600 }, // Cache for 1 hour
  });
  return res.json();
}

Client Component Fetch

"use client";

import useSWR from "swr";

function UserProfile({ userId }: { userId }) {
  const { data, error, isLoading } = useSWR(
    "/api/users/" + userId,
    (url) => fetch(url).then((r) => r.json())
  );

  if (isLoading) return <Skeleton />;
  if (error) return <Error message={error.message} />;

  return <div>{data.name}</div>;
}

Common Pitfalls

Pitfall 1: Using hooks in Server Components

// WRONG - this will error
export default function Page() {
  const [count, setCount] = useState(0); // Error: useState is a Client hook
  return <div>{count}</div>;
}

// CORRECT - add "use client" or extract to a Client Component
"use client";
export default function Counter() {
  const [count, setCount] = useState(0);
  return <button  => setCount(count + 1)}>{count}</button>;
}

Pitfall 2: Importing a Server Component into a Client Component

Free Resource

Free Cloud Architecture Checklist

A 47-point checklist covering security, scalability, cost optimization, and disaster recovery for production cloud environments.

Download the Checklist
// WRONG - ServerComponent becomes a Client Component
"use client";
import { ServerComponent } from "./ServerComponent"; // This is now client!

// CORRECT - pass as children
"use client";
function ClientWrapper({ children }: { children: React.ReactNode }) {
  return <div className="interactive">{children}</div>;
}

// In a Server Component parent:
<ClientWrapper>
  <ServerComponent />  {/* Stays as Server Component */}
</ClientWrapper>

Pitfall 3: Serialization

Props passed from Server to Client Components must be serializable (JSON). No functions, no classes, no Dates:

// WRONG
<ClientComponent  date={new Date()} />

// CORRECT
<ClientComponent dateString={date.toISOString()} />
ProductionWeb ServerApp ServerDatabaseMonitoringStagingWeb ServerApp ServerDatabaseVLANBackupStorage3-2-1 Rule

Server infrastructure: production and staging environments connected via VLAN with offsite backups.

Decision Flowchart

  1. Does it need onClick, onChange, or any event handler? -> Client Component
  2. Does it use useState, useEffect, or useContext? -> Client Component
  3. Does it use browser APIs (window, localStorage)? -> Client Component
  4. Does it only display data? -> Server Component
  5. Does it fetch data from a database/API? -> Server Component
  6. Does it use sensitive environment variables? -> Server Component

When in doubt, start with Server Components and only add "use client" when you need interactivity.

At TechSaaS, our company website (www.techsaas.cloud) uses Next.js 15 with the App Router. Most pages are Server Components that fetch from Directus CMS. Only interactive elements like search bars, filter dropdowns, and language selectors are Client Components.

Need help with Next.js architecture? Contact [email protected].

#nextjs#react#server-components#client-components#frontend

Related Service

Cloud Solutions

Let our experts help you build the right technology strategy for your business.

Need help with tutorials?

TechSaaS provides expert consulting and managed services for cloud infrastructure, DevOps, and AI/ML operations.

We Will Build You a Demo Site — For Free

Like it? Pay us. Do not like it? Walk away, zero complaints. You will spend way less than hiring developers or any agency.

47+ companies trusted us
99.99% uptime
< 48hr response

No spam. No contracts. Just a free demo.