Server & Client Components

Understand React Server Components and when to use client components

React Server Components (RSC)

Next.js uses React Server Components by default in the App Router. Server Components render on the server, reduce client-side JavaScript, and can directly access backend resources like databases and file systems.

🖥️ Server Components (Default)

  • ✅ Fetch data directly (no useEffect)
  • ✅ Access backend resources (DB, filesystem)
  • ✅ Keep sensitive info on server (API keys)
  • ✅ Reduce client bundle size
  • ❌ Cannot use hooks (useState, useEffect)
  • ❌ Cannot use browser APIs
  • ❌ Cannot add event listeners

💻 Client Components ('use client')

  • ✅ Use React hooks (useState, useEffect)
  • ✅ Add event listeners (onClick, onChange)
  • ✅ Access browser APIs (localStorage, window)
  • ✅ Use third-party hooks libraries
  • ❌ Cannot be async functions
  • ❌ Increase client bundle size

Server Component Example

// app/users/page.tsx - Server Component (default)
// No 'use client' directive needed

import { db } from '@/lib/database';

// This component runs only on the server
export default async function UsersPage() {
  // Direct database access - no API needed!
  const users = await db.user.findMany();
  
  // Async/await works directly in the component
  const stats = await fetch('https://api.example.com/stats', {
    headers: {
      // Safe to use - never sent to client
      Authorization: `Bearer ${process.env.SECRET_API_KEY}`,
    },
  }).then(res => res.json());

  return (
    <div>
      <h1>Users ({stats.total})</h1>
      <ul>
        {users.map(user => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </div>
  );
}

// Zero client-side JavaScript for this component!

Client Component Example

'use client'; // This directive makes it a Client Component

import { useState, useEffect } from 'react';

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

  // useEffect works in Client Components
  useEffect(() => {
    setMounted(true);
    // Access browser APIs
    const saved = localStorage.getItem('count');
    if (saved) setCount(parseInt(saved));
  }, []);

  const increment = () => {
    setCount(c => c + 1);
    localStorage.setItem('count', String(count + 1));
  };

  if (!mounted) return null; // Avoid hydration mismatch

  return (
    <button onClick={increment}>
      Count: {count}
    </button>
  );
}

Composition Patterns

// ✅ CORRECT: Pass Server Components as children
// app/dashboard/page.tsx (Server Component)
import ClientWrapper from './ClientWrapper';
import ServerData from './ServerData';

export default async function Dashboard() {
  return (
    <ClientWrapper>
      {/* ServerData stays a Server Component */}
      <ServerData />
    </ClientWrapper>
  );
}

// ClientWrapper.tsx
'use client';
export default function ClientWrapper({ 
  children 
}: { 
  children: React.ReactNode 
}) {
  const [isOpen, setIsOpen] = useState(false);
  return (
    <div>
      <button onClick={() => setIsOpen(!isOpen)}>Toggle</button>
      {isOpen && children}
    </div>
  );
}

// ❌ WRONG: Importing Server Component into Client Component
'use client';
import ServerComponent from './ServerComponent'; // This becomes a Client Component!

When to Use Each

Use Case Component Type
Fetch data 🖥️ Server
Access backend resources 🖥️ Server
Keep sensitive data secure 🖥️ Server
Add interactivity (onClick, onChange) 💻 Client
Use state (useState, useReducer) 💻 Client
Use lifecycle effects (useEffect) 💻 Client
Use browser-only APIs 💻 Client
Use React Context 💻 Client

Mixing Components

// Best Practice: Keep Client Components at the leaves

// ❌ BAD: Large Client Component wrapping everything
'use client';
export default function Page() {
  const [filter, setFilter] = useState('');
  return (
    <div>
      <input onChange={e => setFilter(e.target.value)} />
      <DataTable filter={filter} /> {/* Now forced to be client */}
    </div>
  );
}

// ✅ GOOD: Small Client Component for interactivity only
// SearchFilter.tsx
'use client';
export function SearchFilter({ onFilter }: { onFilter: (v: string) => void }) {
  return <input onChange={e => onFilter(e.target.value)} />;
}

// page.tsx (Server Component)
import { SearchFilter } from './SearchFilter';
import { DataTable } from './DataTable'; // Stays Server Component

export default async function Page() {
  const data = await fetchData();
  return (
    <div>
      <SearchFilter onFilter={handleFilter} />
      <DataTable data={data} />
    </div>
  );
}

Third-Party Libraries

// Many libraries use hooks and need 'use client'
// Create wrapper components for third-party components

// components/ChartWrapper.tsx
'use client';

import { LineChart } from 'some-chart-library';

export function ChartWrapper({ data }: { data: number[] }) {
  return <LineChart data={data} />;
}

// Use in Server Component
import { ChartWrapper } from './ChartWrapper';

export default async function Dashboard() {
  const data = await fetchChartData(); // Server-side fetch
  return <ChartWrapper data={data} />; // Client-side render
}

📖 Server Components Documentation →

⚡ Key Takeaways

  • • Default to Server Components - only use 'use client' when needed
  • • Keep Client Components small and at the leaf nodes
  • • Pass Server Components as children to Client Components
  • • Don't import Server Components into Client Components
  • • Wrap third-party libraries that need hooks