Skip to main content
Adzbyte
Development

Server Components vs Client Components: A Practical Decision Framework

Adrian Saycon
Adrian Saycon
March 17, 20263 min read
Server Components vs Client Components: A Practical Decision Framework

Every time I create a new component in Next.js, the same question comes up: server or client? The official docs bury the practical guidance under pages of theory. Here’s the decision framework I actually use.

The Default: Server Components

Server Components are the default in Next.js App Router. No 'use client' directive means Server Component. The benefits are concrete:

  • Zero JavaScript sent to the browser for that component
  • Direct access to server resources (database, file system, env vars)
  • Async/await works directly in the component body
  • Sensitive logic never reaches the client

Start every component as a Server Component. Only add 'use client' when you have a specific reason.

When You Need Client Components

You need 'use client' when your component uses: useState/useReducer, useEffect, event handlers (onClick, onChange), browser APIs (window, localStorage), custom hooks that depend on any of these, or third-party components requiring client rendering. That’s the complete list.

The Boundary Strategy

Push the 'use client' boundary as low as possible. Don’t mark a page as a Client Component because one button needs an onClick handler. Extract the button into its own Client Component instead.

// app/blog/[slug]/page.tsx — Server Component
import { fetchPost } from '@/lib/wordpress';
import { ShareButton } from '@/components/share-button';

export default async function BlogPost({ params }: { params: { slug: string } }) {
  const post = await fetchPost(params.slug);

  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
      <ShareButton url={`/blog/${post.slug}`} title={post.title} />
    </article>
  );
}
// components/share-button.tsx — Client Component
'use client';

import { useState } from 'react';

export function ShareButton({ url, title }: { url: string; title: string }) {
  const [copied, setCopied] = useState(false);

  const handleShare = async () => {
    await navigator.clipboard.writeText(window.location.origin + url);
    setCopied(true);
    setTimeout(() => setCopied(false), 2000);
  };

  return (
    <button onClick={handleShare}>
      {copied ? 'Copied!' : 'Share'}
    </button>
  );
}

The blog page fetches data on the server with zero client JavaScript for the article content, shipping only the small ShareButton to the browser.

Passing Server Data to Client Components

Server Components pass data to Client Components through serializable props — the bridge between the two worlds:

export default async function Dashboard() {
  const stats = await fetchDashboardStats();

  return (
    <div>
      <h1>Dashboard</h1>
      <InteractiveChart data={stats.chartData} />
      <StatsSummary stats={stats.summary} /> {/* Stays a Server Component */}
    </div>
  );
}

You can’t pass functions, class instances, or other non-serializable values across this boundary.

Common Mistakes

Making entire pages Client Components. A developer needs one useState call, so they slap 'use client' on the page. Now the entire page and all children are Client Components. Extract the interactive piece instead.

Importing a Server Component into a Client Component. When a Client Component imports another component, that import becomes a Client Component too. The only way to nest Server Components inside Client Components is through the children prop:

// This WORKS — Server Component passed as children
<ClientSidebar>
  <ServerNavLinks />  {/* Stays a Server Component */}
</ClientSidebar>

// This DOESN'T preserve server rendering
'use client';
import { ServerNavLinks } from './server-nav-links'; // Now a Client Component!

Using 'use client' to fix async component errors. Client Components can’t be async. The fix is to ensure you’re in a Server Component context or move data fetching up.

The Mental Model

Think of your component tree as having a “client boundary” line. Everything above runs on the server. Everything below ships to the browser. Push that line as far down as possible so the maximum amount of UI renders on the server with zero JavaScript cost.

Adrian Saycon

Written by

Adrian Saycon

A developer with a passion for emerging technologies, Adrian Saycon focuses on transforming the latest tech trends into great, functional products.

Discussion (0)

Sign in to join the discussion

No comments yet. Be the first to share your thoughts.