Back to Articles
December 28, 2024 10 min

React Server Components vs Client Components

#React
#Next.js
#Opinion

React Server Components vs Client Components

React Server Components (RSC) introduced a new paradigm in React. In this article, I’ll explain when to use each type with practical examples.

What Are Server Components?

Server Components are React components that render on the server and send HTML to the client. They have access to server-side resources and don’t increase your JavaScript bundle size.

Key Characteristics

  • Render on the server only
  • Can directly access databases, APIs, file system
  • Cannot use client hooks (useState, useEffect, etc.)
  • Cannot use browser APIs
  • Zero JavaScript sent to client (unless they import Client Components)

What Are Client Components?

Client Components are traditional React components that run in the browser. They’re marked with 'use client' directive.

Key Characteristics

  • Render on both server (initial render) and client (hydration)
  • Can use all React hooks
  • Can access browser APIs (localStorage, window, etc.)
  • Interactive and stateful
  • Add to JavaScript bundle size

When to Use Server Components

1. Data Fetching

Use Server Components for fetching data from databases or APIs:

// app/users/page.tsx
import { prisma } from '@/lib/prisma';

async function UsersPage() {
  // Direct database access! No API needed
  const users = await prisma.user.findMany();

  return (
    <div>
      {users.map(user => (
        <div key={user.id}>{user.name}</div>
      ))}
    </div>
  );
}

export default UsersPage;

Benefits:

  • No API layer needed
  • Faster - data fetched on server
  • No exposed API endpoints
  • Smaller bundle size

2. Accessing Secret Environment Variables

// Server Component - safe to use secrets
async function Dashboard() {
  const apiKey = process.env.SECRET_API_KEY; // Safe!
  const data = await fetch(`https://api.example.com/data`, {
    headers: { 'X-API-Key': apiKey }
  });

  return <div>{/* render data */}</div>;
}

3. Rendering Static Content

//Server Component
function BlogPost({ content }: { content: string }) {
  return (
    <article className="prose">
      <h1>{content.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: content.html }} />
    </article>
  );
}

When to Use Client Components

1. Interactive UI Elements

Use Client Components for interactive elements:

'use client';

import { useState } from 'react';

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

  return (
    <button onClick={() => setCount(count + 1)}>
      Clicked {count} times
    </button>
  );
}

2. Using React Hooks

'use client';

import { useEffect, useState } from 'react';

export function Timer() {
  const [time, setTime] = useState(new Date());

  useEffect(() => {
    const interval = setInterval(() => {
      setTime(new Date());
    }, 1000);

    return () => clearInterval(interval);
  }, []);

  return <div>Current time: {time.toLocaleTimeString()}</div>;
}

3. Browser APIs

'use client';

export function ThemeToggle() {
  const toggle = () => {
    // Access browser APIs
    const theme = localStorage.getItem('theme');
    localStorage.setItem('theme', theme === 'dark' ? 'light' : 'dark');
  };

  return <button onClick={toggle}>Toggle Theme</button>;
}

4. Event Handlers

'use client';

export function SearchBar() {
  return (
    <input 
      type="text"
      onChange={(e) => console.log(e.target.value)}
      onClick={() => alert('Focused!')}
    />
  );
}

Composition Pattern

The best approach is Server Components by default, Client Components when needed:

// app/page.tsx (Server Component)
import { Suspense } from 'react';
import { UserList } from './UserList'; // Server Component
import { SearchBar } from './SearchBar'; // Client Component

async function HomePage() {
  // Server-side data fetch
  const users = await fetchUsers();

  return (
    <div>
      <h1>Users</h1>
      
      {/* Client Component for interactivity */}
      <SearchBar />

      {/* Server Component for rendering */}
      <Suspense fallback={<div>Loading...</div>}>
        <UserList users={users} />
      </Suspense>
    </div>
  );
}

Performance Comparison

AspectServer ComponentsClient Components
Bundle Size✅ Zero JS❌ Adds to bundle
Data Fetching✅ Direct access❌ Needs API
Interactivity❌ Not interactive✅ Fully interactive
Initial Load✅ Faster❌ Slower
Runtime✅ No runtime cost❌ Runtime overhead

Common Patterns

Pattern 1: Server Component Wrapper

// Server Component
async function UserProfile({ id }: { id: string }) {
  const user = await fetchUser(id);

  return (
    <div>
      <h1>{user.name}</h1>
      {/* Client Component for interactivity */}
      <FollowButton userId={user.id} />
    </div>
  );
}

Pattern 2: Passing Props

// Server Component passes data to Client Component
async function PostsPage() {
  const posts = await fetchPosts();

  return <PostList posts={posts} />; // Client Component
}

Pattern 3: Layouts as Server Components

// app/layout.tsx (Server Component)
export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <Header /> {/* Server Component */}
        <main>{children}</main>
        <Footer /> {/* Server Component */}
      </body>
    </html>
  );
}

Decision Tree

Does it need interactivity (onClick, onChange, etc.)?
├─ YES → Use Client Component
└─ NO

    Does it use React hooks (useState, useEffect, etc.)?
    ├─ YES → Use Client Component
    └─ NO

        Does it access browser APIs (localStorage, window)?
        ├─ YES → Use Client Component
        └─ NO → Use Server Component ✅

Best Practices

  1. Server Components by default - Mark 'use client' only when needed
  2. Push Client Components down - Keep them as low as possible in the tree
  3. Pass data as props - Server → Client via props, not imports
  4. Don’t import Server into Client - Will break
  5. Use Suspense boundaries - For better loading states

Common Mistakes

❌ Mistake 1: Using hooks in Server Components

// WRONG - Server Component can't use hooks
async function Page() {
  const [count, setCount] = useState(0); // Error!
  return <div>{count}</div>;
}

❌ Mistake 2: Accessing server-only code in Client Components

'use client';

// WRONG - Can't import Prisma in Client Component
import { prisma } from '@/lib/prisma'; // Error!

✅ Correct Approach

// Server Component
async function Page() {
  const data = await prisma.user.findMany();
  return <ClientComponent data={data} />;
}

Conclusion

Use Server Components when:

  • Fetching data
  • Accessing server resources
  • Rendering static content
  • Improving performance

Use Client Components when:

  • Adding interactivity
  • Using React hooks
  • Accessing browser APIs
  • Handling events

The combination of both creates the most performant and maintainable Next.js applications!