TechLead

Server Components

React Server Components, Server vs Client components, and data fetching patterns

What Are Server Components?

React Server Components (RSC) are a fundamental shift in how React apps work. Instead of sending all component code to the browser, Server Components run only on the server. They can directly access databases, file systems, and APIs without sending that code to the client.

The result: smaller JavaScript bundles, faster page loads, and simpler data fetching. Server Components are fully integrated into frameworks like Next.js 13+ (App Router).

Server vs Client Components

πŸ–₯️ Server Components

  • β€’ Run only on the server
  • β€’ Can access databases directly
  • β€’ Zero bundle size impact
  • β€’ Can use async/await
  • β€’ Cannot use state or effects
  • β€’ Cannot use browser APIs
  • β€’ Default in Next.js App Router

🌐 Client Components

  • β€’ Run in the browser
  • β€’ Can use useState, useEffect
  • β€’ Can handle user interactions
  • β€’ Can access browser APIs
  • β€’ Add to JavaScript bundle
  • β€’ Need "use client" directive
  • β€’ Traditional React behavior

Server Component Basics

Server Components are the default in Next.js App Router. They can be async and directly fetch data:

// app/users/page.jsx β€” Server Component (default)
// No "use client" needed!

async function UsersPage() {
  // Fetch data directly β€” no useEffect, no loading state boilerplate
  const users = await fetch('https://api.example.com/users').then(r => r.json());

  return (
    <div>
      <h1>Users</h1>
      <ul>
        {users.map(user => (
          <li key={user.id}>{user.name} β€” {user.email}</li>
        ))}
      </ul>
    </div>
  );
}

export default UsersPage;

Direct Database Access

Server Components can query databases directly β€” the database code never reaches the client:

// app/posts/page.jsx β€” Server Component
import { db } from '@/lib/database';

async function PostsPage() {
  // Query the database directly β€” this code stays on the server
  const posts = await db.post.findMany({
    include: { author: true },
    orderBy: { createdAt: 'desc' },
  });

  return (
    <div>
      <h1>Blog Posts</h1>
      {posts.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>By {post.author.name}</p>
          <p>{post.excerpt}</p>
        </article>
      ))}
    </div>
  );
}

Client Components with "use client"

When you need interactivity, state, or browser APIs, add the "use client" directive at the top of the file:

// components/LikeButton.jsx β€” Client Component
'use client';

import { useState } from 'react';

export function LikeButton({ postId, initialLikes }) {
  const [likes, setLikes] = useState(initialLikes);
  const [isLiked, setIsLiked] = useState(false);

  async function handleLike() {
    setIsLiked(!isLiked);
    setLikes(prev => isLiked ? prev - 1 : prev + 1);

    await fetch(`/api/posts/${postId}/like`, { method: 'POST' });
  }

  return (
    <button onClick={handleLike} className="flex items-center gap-2">
      {isLiked ? '❀️' : '🀍'} {likes}
    </button>
  );
}

Composing Server and Client Components

The key pattern: Server Components can import and render Client Components, passing server-fetched data as props:

// app/posts/[id]/page.jsx β€” Server Component
import { db } from '@/lib/database';
import { LikeButton } from '@/components/LikeButton';
import { CommentSection } from '@/components/CommentSection';

async function PostPage({ params }) {
  const post = await db.post.findUnique({
    where: { id: params.id },
    include: { author: true },
  });

  return (
    <article>
      {/* Static content rendered on server β€” zero JS */}
      <h1>{post.title}</h1>
      <p>By {post.author.name}</p>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />

      {/* Interactive parts are Client Components */}
      <LikeButton postId={post.id} initialLikes={post.likes} />
      <CommentSection postId={post.id} />
    </article>
  );
}

Passing Server Data to Client Components

Data flows from Server β†’ Client via props. Only serializable data (JSON) can be passed:

// βœ… Can pass: strings, numbers, arrays, plain objects, dates
// ❌ Cannot pass: functions, classes, Map, Set, symbols

// Server Component
async function Dashboard() {
  const stats = await getStats(); // { users: 100, revenue: 5000 }
  const recentOrders = await getRecentOrders(); // array of objects

  return (
    <div>
      <h1>Dashboard</h1>
      {/* Pass serializable data as props */}
      <StatsChart data={stats} />
      <OrderTable orders={recentOrders} />
    </div>
  );
}

// Client Component
'use client';
function StatsChart({ data }) {
  // Can use hooks and interactivity with the server data
  const [period, setPeriod] = useState('week');
  return <Chart data={data} period={period} />;
}

When to Use Each

Use Case Component Type
Fetching dataπŸ–₯️ Server
Reading database / filesystemπŸ–₯️ Server
Using API keys / secretsπŸ–₯️ Server
Static content renderingπŸ–₯️ Server
onClick, onChange handlers🌐 Client
useState, useEffect, useRef🌐 Client
Browser APIs (localStorage, etc.)🌐 Client
Real-time / WebSocket🌐 Client

βœ… Best Practices

  • β€’ Keep components as Server Components by default β€” only add "use client" when needed
  • β€’ Push Client Component boundaries as far down the tree as possible
  • β€’ Pass fetched data from Server Components to Client Components via props
  • β€’ Use loading.tsx and error.tsx files for automatic Suspense/Error boundaries in Next.js
  • β€’ Never import server-only code in Client Components
  • β€’ Use the server-only npm package to prevent accidental client imports