Server Components vs Client Components in Next.js 14+

6 min read

Introduction

With the arrival of Next.js 13 and its stabilization in Next.js 14+, React Server Components (RSC) have fundamentally changed how we build React applications.

But when should you use Server Components and when Client Components?

In this post, I’ll explain it clearly and practically, with real-world examples and proven patterns.

What Are Server Components?

Server Components are React components that render exclusively on the server. They don’t ship JavaScript to the client, resulting in smaller bundles, faster initial loads, and better scalability.

In Next.js 14+, Server Components are the default in the App Router.

Key Features

  • Execute only on the server
  • Can directly access databases, file systems, and private APIs
  • Don’t increase client bundle size
  • Improve performance and SEO
  • Support streaming and partial rendering
  • Are the default behavior in Next.js 14+ (App Router)

By default, components in the app directory are Server Components unless you explicitly mark them with 'use client'.

Example

async function PostsPage() {
  const posts = await fetch('https://api.example.com/posts').then(r => r.json());
  return (
    <div>
      <h1>Posts</h1>
      {posts.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.excerpt}</p>
        </article>
      ))}
    </div>
  );
}

export default PostsPage;

This component runs entirely on the server. No JavaScript from this component is sent to the browser.

What Are Client Components?

Client Components are traditional React components that run in the browser. They must be explicitly marked with the 'use client' directive.

Client Components are necessary whenever you need interactivity or access to browser APIs.

Key Features

  • Execute in the browser
  • Can use React hooks (useState, useEffect, useContext, etc.)
  • Handle user interactions
  • Access browser APIs (window, localStorage, etc.)
  • Require the 'use client' directive
  • Increase the JavaScript bundle sent to the client
'use client'

import { useState } from 'react';

export function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        Increment
      </button>
    </div>
  );
}

Server vs Client Components: Direct Comparison

Feature

Server Component

Client Component

Rendering

Server

Browser

JavaScript in bundle

❌ No

✅ Yes

React Hooks

❌ No

✅ Yes

Database access

✅ Direct

❌ Only via API

Interactivity

❌ No

✅ Yes

Event handlers

❌ No

✅ Yes

SEO

✅ Excellent

⚠️ Depends

Default in App Router

✅ Yes

❌ No ('use client')

When to Use Server Components

  • 📊 You need to fetch data
  • 🗄️ You access a database directly
  • 🔐 You handle sensitive information (API keys, tokens)
  • 📦 You want to reduce bundle size
  • 🚀 You want better initial performance
  • 📈 SEO is important

Typical Use Cases

  • Blog posts and content pages
  • Product listings in e-commerce
  • Dashboards with server-side data
  • Static layouts and templates
  • CMS-driven pages

When to Use Client Components

  • 🖱️ You handle user events (onClick, onChange)
  • 📱 You use React hooks (useState, useEffect, useContext)
  • 🎨 You need interactive animations
  • 🌐 You access browser APIs (localStorage, window)
  • 🎯 You implement interactive forms
  • 🔄 You manage local state

Typical Use Cases

  • Buttons and counters
  • Forms and validations
  • Modals and dialogs
  • Carousels and sliders
  • Theme switchers
  • Drag-and-drop components

Core Principle: Server First, Client When Needed

Everything should be a Server Component by default. Use Client Components only when interactivity is required.

This approach minimizes JavaScript sent to the browser and maximizes performance.

Pattern 1: Composing Server and Client Components

Server Components can render Client Components, but not the other way around.

✅ Correct Approach

// app/page.tsx (Server Component)
import { Counter } from '@/components/Counter';

async function HomePage() {
  const data = await fetchData();

  return (
    <div>
      <h1>Server Component Data</h1>
      <p>{data.title}</p>
      <Counter />
    </div>
  );
}

export default HomePage;

❌ Incorrect Approach

'use client'

import { ServerData } from './ServerData';

export function ClientWrapper() {
  return <ServerData />;
}

❗ Client Components cannot import Server Components.

Pattern 2: Minimize Client Components

Keep most of your application as Server Components and extract only the interactive parts.

Example: Blog Post Page

async function PostPage({ params }) {
  const post = await getPost(params.slug);

  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.date}</p>
      <div>{post.content}</div>

      <LikeButton postId={post.id} />
      <CommentForm postId={post.id} />
    </article>
  );
}
'use client'

import { useState } from 'react';

export function LikeButton({ postId }) {
  const [liked, setLiked] = useState(false);

  return (
    <button onClick={() => setLiked(!liked)}>
      {liked ? '❤️' : '🤍'} Like
    </button>
  );
}

Pattern 3: Passing Server Components as Children

You can pass Server Components as children to Client Components.

'use client'

import { useState } from 'react';

export function ClientWrapper({ children }) {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <div>
      <button onClick={() => setIsOpen(!isOpen)}>Toggle</button>
      {isOpen && children}
    </div>
  );
}
import { ClientWrapper } from '@/components/ClientWrapper';

async function HomePage() {
  const data = await fetchData();

  return (
    <ClientWrapper>
      <div>
        <h2>{data.title}</h2>
        <p>{data.description}</p>
      </div>
    </ClientWrapper>
  );
}

Best Practices

1. Server Components by Default

Always start with Server Components. Add 'use client' only when necessary.

2. Reduce Client-Side JavaScript

Less JavaScript in the browser = faster load times and better UX.

3. Use Composition Patterns

Combine Server and Client Components strategically.

4. Use Serializable Props

Client Component props must be serializable (no functions, raw Dates, etc.).

5. Fetch Data on the Server

Prefer Server Components over useEffect for data fetching.

Conclusion

Server Components in Next.js 14+ represent a paradigm shift in how we build React applications. The key takeaways are:

  • 🎯 Use Server Components by default for better performance and SEO
  • 🎨 Extract Client Components only when you need interactivity
  • 🔧 Compose both intelligently to leverage the best of both worlds
  • 📦 Reduce JavaScript bundle size for faster applications

Remember: not everything needs to be interactive, and that's good news for your application's performance.

By following these patterns and best practices, you'll build faster, more efficient React applications that provide a better user experience.

Additional Resources

Next.js Documentation - Server Components

React Documentation - Server Components

Vercel - Understanding Server Components

Next.js App Router Migration Guide