Server Components and Full-Stack React NEXT.JS 13+

1. React Server Components Fundamentals

Concept Description Characteristic Use Case
Server Components Components that run only on server No JavaScript sent to client, async by default Data fetching, database queries, backend logic
Client Components Traditional React components Run on both server (SSR) and client, need 'use client' Interactivity, hooks, browser APIs, event handlers
Zero Bundle Impact Server component code not in bundle Smaller JavaScript payload Large dependencies, utilities, formatters
Direct Backend Access Access databases, file system directly No API route needed Database queries, file reads, server-only logic
Async Components Can use async/await in component Natural data fetching pattern Fetch data directly in component body
Streaming Stream HTML progressively Works with Suspense boundaries Faster initial page load, progressive rendering

Example: Server Component basics

// app/page.tsx - Server Component by default (Next.js 13+)
// No 'use client' directive = Server Component

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

// Server components can be async!
async function ProductList() {
  // Direct database access - no API route needed
  const products = await db.product.findMany({
    include: { category: true }
  });
  
  return (
    <div>
      <h1>Products</h1>
      <ul>
        {products.map(product => (
          <li key={product.id}>
            {product.name} - ${product.price}
            <span>{product.category.name}</span>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default ProductList;

// Server Component characteristics:
// ✓ Can use async/await
// ✓ Direct database/filesystem access
// ✓ No JavaScript sent to client
// ✓ Can import server-only code
// ✓ Runs only on server
// ✗ Cannot use hooks (useState, useEffect, etc.)
// ✗ Cannot use browser APIs
// ✗ Cannot have event handlers

Example: Client Component with 'use client'

// components/Counter.tsx - Client Component
'use client'; // This directive marks it as a Client Component

import { useState } from 'react';

export function Counter() {
  // Can use hooks - runs on client
  const [count, setCount] = useState(0);
  
  return (
    <div>
      <p>Count: {count}</p>
      {/* Event handlers work */}
      <button onClick={() => setCount(count + 1)}>
        Increment
      </button>
    </div>
  );
}

// Client Component characteristics:
// ✓ Can use hooks (useState, useEffect, etc.)
// ✓ Can use browser APIs
// ✓ Can have event handlers
// ✓ Interactive features
// ✗ Cannot be async
// ✗ JavaScript sent to client
// ✗ Cannot directly access server resources
Server vs Client Components: Server Components are the default in Next.js App Router, use 'use client' for interactivity, server components have zero bundle impact, can mix both in same app, server components can import client components (but not vice versa).

2. Client vs Server Component Boundaries

Rule Allowed Not Allowed Why
Importing Server can import Client Client cannot import Server Server code shouldn't run on client
Props Pass serializable props to Client Pass functions, class instances Props must cross network boundary
Children Pattern Pass Server as children to Client Import Server into Client Composition pattern preserves boundaries
Context Client Components can provide context Server Components cannot use context Context requires client-side state
Third-party Mark packages as 'use client' if needed Import packages without checking Many packages assume client environment

Example: Component composition patterns

// ✓ CORRECT: Server Component imports Client Component
// app/page.tsx (Server Component)
import { Counter } from '@/components/Counter'; // Client Component

export default function Page() {
  return (
    <div>
      <h1>My Page</h1>
      <Counter /> {/* Client Component works here */}
    </div>
  );
}

// ✗ WRONG: Client Component cannot import Server Component
// components/ClientComponent.tsx
'use client';
import { ServerData } from './ServerComponent'; // ERROR!

// ✓ CORRECT: Use composition with children prop
// components/ClientWrapper.tsx
'use client';

export function ClientWrapper({ children }) {
  const [state, setState] = useState(false);
  
  return (
    <div onClick={() => setState(!state)}>
      {children} {/* Server Component passed as children */}
    </div>
  );
}

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

async function ServerData() {
  const data = await fetchFromDatabase();
  return <div>{data}</div>;
}

export default function Page() {
  return (
    <ClientWrapper>
      <ServerData /> {/* Server Component passed to Client via children */}
    </ClientWrapper>
  );
}

Example: Serializable props requirement

// ✓ CORRECT: Serializable props
// app/page.tsx (Server Component)
<ClientComponent 
  name="John"
  age={30}
  items={['a', 'b', 'c']}
  config={{ theme: 'dark', locale: 'en' }}
/>

// ✗ WRONG: Non-serializable props
<ClientComponent 
  onClick={() => console.log('click')} // Functions can't be serialized
  date={new Date()} // Dates lose type information
  callback={myFunction} // Functions not allowed
/>

// ✓ CORRECT: Define handlers in Client Component
'use client';

export function ClientComponent({ name, age }) {
  // Define event handlers in client component
  const handleClick = () => {
    console.log(`${name} clicked, age: ${age}`);
  };
  
  return <button onClick={handleClick}>{name}</button>;
}

// ✓ CORRECT: Server Actions for server-side logic
// app/actions.ts
'use server';

export async function updateUser(formData) {
  // Server-side logic
  const name = formData.get('name');
  await db.user.update({ name });
}

// Client Component can call Server Action
'use client';
import { updateUser } from './actions';

export function Form() {
  return (
    <form action={updateUser}>
      <input name="name" />
      <button type="submit">Save</button>
    </form>
  );
}
Warning: Server Components cannot import Client Components as modules - use children prop pattern. Props must be serializable (no functions, class instances). Third-party packages may need 'use client' wrapper. Test boundaries carefully.

3. Data Fetching in Server Components

Pattern Implementation Benefit Use Case
Async Components async function Component() Natural async/await syntax Database queries, API calls, file reads
Parallel Fetching Promise.all([fetch1, fetch2]) Multiple requests simultaneously Independent data sources
Sequential Fetching Await each fetch separately Dependent requests Second request needs first's result
Streaming with Suspense Wrap slow components in Suspense Progressive page rendering Show fast content first, stream slow data
Request Memoization React dedupes identical requests Automatic caching during render Multiple components need same data
Server-only Code import 'server-only' Prevent client-side import Database clients, secret keys, server utilities

Example: Data fetching patterns

// Async Server Component
async function UserProfile({ userId }) {
  // Direct database access
  const user = await db.user.findUnique({
    where: { id: userId },
    include: { posts: true, followers: true }
  });
  
  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
      <p>Posts: {user.posts.length}</p>
      <p>Followers: {user.followers.length}</p>
    </div>
  );
}

// Parallel data fetching
async function Dashboard() {
  // Fetch multiple resources in parallel
  const [user, posts, stats] = await Promise.all([
    db.user.findFirst(),
    db.post.findMany({ take: 10 }),
    db.analytics.getStats()
  ]);
  
  return (
    <div>
      <UserCard user={user} />
      <PostList posts={posts} />
      <StatsWidget stats={stats} />
    </div>
  );
}

// Sequential data fetching (when dependent)
async function PostWithComments({ postId }) {
  // First, get the post
  const post = await db.post.findUnique({
    where: { id: postId }
  });
  
  // Then, get comments (needs post.id)
  const comments = await db.comment.findMany({
    where: { postId: post.id }
  });
  
  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
      <CommentList comments={comments} />
    </article>
  );
}

Example: Streaming with Suspense

// app/page.tsx
import { Suspense } from 'react';

// Fast component
async function QuickData() {
  const data = await fetch('https://api.example.com/quick');
  return <div>Quick: {data}</div>;
}

// Slow component
async function SlowData() {
  const data = await fetch('https://api.example.com/slow');
  return <div>Slow: {data}</div>;
}

export default function Page() {
  return (
    <div>
      <h1>Dashboard</h1>
      
      {/* Quick data renders immediately */}
      <Suspense fallback={<div>Loading quick data...</div>}>
        <QuickData />
      </Suspense>
      
      {/* Slow data streams in later */}
      <Suspense fallback={<div>Loading slow data...</div>}>
        <SlowData />
      </Suspense>
    </div>
  );
}

// Result: Page streams progressively
// 1. Shell with "Loading..." fallbacks sent immediately
// 2. QuickData arrives and replaces first fallback
// 3. SlowData arrives later and replaces second fallback

Example: Server-only code protection

// lib/database.ts
import 'server-only'; // Prevents client-side import

import { PrismaClient } from '@prisma/client';

// This will only run on server
export const db = new PrismaClient();

// Server-only utilities
export async function getSecretData() {
  // Safe to use environment variables
  const apiKey = process.env.SECRET_API_KEY;
  return fetchWithKey(apiKey);
}

// If you try to import this in a Client Component:
// 'use client';
// import { db } from '@/lib/database'; // ERROR at build time!

// lib/utils.ts - Shared utilities
// No 'server-only' import = can be used anywhere
export function formatDate(date) {
  return date.toLocaleDateString();
}

// Use 'server-only' package for:
// - Database clients
// - API clients with secrets
// - Server-only utilities
// - Code that must never be exposed to client
Data Fetching Best Practices: Fetch data at component level (not page level), use Suspense for streaming, parallelize independent requests, use request memoization (React dedupes), protect server-only code with 'server-only' package, cache appropriately with Next.js caching strategies.

4. Server Component Performance Benefits

Benefit Impact Measurement Example
Reduced Bundle Size Server code not sent to client Smaller JavaScript payloads 100KB library → 0KB on client
Faster Initial Load Less JavaScript to parse/execute Better Time to Interactive (TTI) Heavy formatters, utilities stay on server
Improved FCP HTML streams immediately First Contentful Paint Static content shows instantly
Better SEO Fully rendered HTML for crawlers Complete content in initial HTML No waiting for client-side fetch
Reduced Waterfalls Fetch data on server in parallel Fewer round trips No client → server → database chain
Direct Backend Access No API routes needed Fewer HTTP requests Direct database queries

Example: Bundle size comparison

// ❌ Client Component approach (old way)
'use client';
import moment from 'moment'; // 67KB gzipped!
import { remark } from 'remark'; // 50KB gzipped!

export function BlogPost({ content, date }) {
  const formattedDate = moment(date).format('MMMM Do, YYYY');
  const html = remark().processSync(content);
  
  return (
    <article>
      <time>{formattedDate}</time>
      <div dangerouslySetInnerHTML={{ __html: html }} />
    </article>
  );
}
// Total bundle increase: ~117KB just for this component!

// ✅ Server Component approach (new way)
// No 'use client' = Server Component
import moment from 'moment'; // Runs on server only
import { remark } from 'remark'; // Runs on server only

export async function BlogPost({ postId }) {
  const post = await db.post.findUnique({ where: { id: postId } });
  
  const formattedDate = moment(post.date).format('MMMM Do, YYYY');
  const html = remark().processSync(post.content);
  
  return (
    <article>
      <time>{formattedDate}</time>
      <div dangerouslySetInnerHTML={{ __html: html }} />
    </article>
  );
}
// Client bundle increase: 0KB! All processing on server.

Example: Performance optimization strategies

// Optimize with strategic Client/Server split

// Server Component - heavy processing
async function ProductGrid() {
  const products = await db.product.findMany({
    include: { reviews: true }
  });
  
  // Heavy computation on server
  const productsWithRatings = products.map(product => ({
    ...product,
    averageRating: calculateAverage(product.reviews),
    reviewCount: product.reviews.length
  }));
  
  return (
    <div className="grid">
      {productsWithRatings.map(product => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  );
}

// Client Component - only interactive parts
'use client';

export function ProductCard({ product }) {
  const [isFavorite, setIsFavorite] = useState(false);
  
  return (
    <div className="card">
      {/* Static content from server */}
      <h3>{product.name}</h3>
      <p>${product.price}</p>
      <div>⭐ {product.averageRating} ({product.reviewCount})</div>
      
      {/* Interactive part on client */}
      <button onClick={() => setIsFavorite(!isFavorite)}>
        {isFavorite ? '❤️' : '🤍'} Favorite
      </button>
    </div>
  );
}

// Benefits:
// ✓ Heavy computation (calculateAverage) runs on server
// ✓ Database query stays on server
// ✓ Only small interactive component sent to client
// ✓ Better performance overall
Performance Impact: Server Components significantly reduce bundle size, improve initial load times, enable better caching strategies, reduce network waterfalls, improve Core Web Vitals (LCP, FCP, TTI), benefit SEO with complete HTML.

5. Hydration and Client-side State Management

Concept Description Consideration Solution
Selective Hydration Hydrate client components only Server component HTML stays static Faster hydration, less JavaScript execution
State Management Client components manage state Server components are stateless Use Context, Zustand, Redux in client components
Server → Client Data Pass data via props Props must be serializable JSON-serializable data only
Client State Hydration Initialize client state from server data Avoid hydration mismatches Use useEffect for client-only state
Progressive Enhancement Works without JavaScript first Server Components always work Add interactivity with Client Components

Example: State management patterns

// Server Component fetches data
async function UserDashboard({ userId }) {
  const user = await db.user.findUnique({
    where: { id: userId },
    include: { preferences: true }
  });
  
  // Pass data to Client Component
  return (
    <div>
      <h1>Welcome, {user.name}</h1>
      <UserSettings initialSettings={user.preferences} />
    </div>
  );
}

// Client Component manages state
'use client';

export function UserSettings({ initialSettings }) {
  // Initialize state from server data
  const [settings, setSettings] = useState(initialSettings);
  const [isSaving, setIsSaving] = useState(false);
  
  const handleSave = async () => {
    setIsSaving(true);
    await fetch('/api/settings', {
      method: 'POST',
      body: JSON.stringify(settings)
    });
    setIsSaving(false);
  };
  
  return (
    <div>
      <label>
        Theme:
        <select 
          value={settings.theme}
          onChange={(e) => setSettings({...settings, theme: e.target.value})}
        >
          <option value="light">Light</option>
          <option value="dark">Dark</option>
        </select>
      </label>
      
      <button onClick={handleSave} disabled={isSaving}>
        {isSaving ? 'Saving...' : 'Save Settings'}
      </button>
    </div>
  );
}

Example: Context providers in Client Components

// app/layout.tsx - Server Component
import { Providers } from './providers';

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        {/* Providers must be Client Component */}
        <Providers>
          {children}
        </Providers>
      </body>
    </html>
  );
}

// app/providers.tsx - Client Component
'use client';

import { createContext, useContext, useState } from 'react';

const ThemeContext = createContext();

export function Providers({ children }) {
  const [theme, setTheme] = useState('light');
  
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

export function useTheme() {
  return useContext(ThemeContext);
}

// Any Client Component can now use theme
'use client';
import { useTheme } from './providers';

export function ThemeToggle() {
  const { theme, setTheme } = useTheme();
  
  return (
    <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
      Current: {theme}
    </button>
  );
}

Example: Avoiding hydration mismatches

// ❌ WRONG: Causes hydration mismatch
'use client';

export function TimeDisplay() {
  // Server renders one time, client hydrates with different time
  return <div>Current time: {new Date().toLocaleTimeString()}</div>;
  // ERROR: Server HTML doesn't match client hydration
}

// ✅ CORRECT: Client-only rendering
'use client';

export function TimeDisplay() {
  const [time, setTime] = useState(null);
  
  useEffect(() => {
    // Only runs on client
    setTime(new Date().toLocaleTimeString());
    
    const interval = setInterval(() => {
      setTime(new Date().toLocaleTimeString());
    }, 1000);
    
    return () => clearInterval(interval);
  }, []);
  
  // Show nothing during SSR, then show time on client
  if (!time) return <div>Loading time...</div>;
  
  return <div>Current time: {time}</div>;
}

// ✅ ALTERNATIVE: Suppress hydration warning (use sparingly)
'use client';

export function TimeDisplay() {
  return (
    <div suppressHydrationWarning>
      Current time: {new Date().toLocaleTimeString()}
    </div>
  );
}
Hydration Warning: Client and server HTML must match during hydration. Use useEffect for client-only code, avoid Date.now() or random values in render, use suppressHydrationWarning sparingly, test hydration carefully.

6. Next.js App Router and Server Components

Feature File Convention Purpose Component Type
Page app/page.tsx Route page component Server by default
Layout app/layout.tsx Shared layout wrapper Server by default
Loading app/loading.tsx Loading UI (Suspense fallback) Server by default
Error app/error.tsx Error boundary UI Must be Client ('use client')
Template app/template.tsx Re-rendered on navigation Server by default
Route Handler app/api/route.ts API endpoint Server-only

Example: Next.js App Router file structure

app/
├── layout.tsx        # Root layout (Server Component)
├── page.tsx          # Home page (Server Component)
├── loading.tsx       # Loading UI (Server Component)
├── error.tsx         # Error boundary (Client Component)
├── providers.tsx     # Context providers (Client Component)

├── dashboard/
│   ├── layout.tsx    # Dashboard layout
│   ├── page.tsx      # Dashboard page
│   └── loading.tsx   # Dashboard loading

├── blog/
│   ├── page.tsx      # Blog list
│   └── [slug]/
│       ├── page.tsx  # Blog post (dynamic route)
│       └── loading.tsx

└── api/
    └── users/
        └── route.ts  # API route handler

// app/layout.tsx - Root Layout
export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>
        <header>
          <nav>...</nav>
        </header>
        <main>{children}</main>
        <footer>...</footer>
      </body>
    </html>
  );
}

// app/page.tsx - Home Page (Server Component)
export default async function HomePage() {
  const posts = await db.post.findMany({ take: 5 });
  
  return (
    <div>
      <h1>Latest Posts</h1>
      <PostList posts={posts} />
    </div>
  );
}

// app/loading.tsx - Loading UI
export default function Loading() {
  return <div>Loading...</div>;
}

// app/error.tsx - Error Boundary (must be Client)
'use client';

export default function Error({ error, reset }) {
  return (
    <div>
      <h2>Something went wrong!</h2>
      <p>{error.message}</p>
      <button onClick={reset}>Try again</button>
    </div>
  );
}

Example: Dynamic routes and data fetching

// app/blog/[slug]/page.tsx - Dynamic route
export default async function BlogPost({ params }) {
  // Params are automatically typed and available
  const post = await db.post.findUnique({
    where: { slug: params.slug }
  });
  
  if (!post) {
    notFound(); // Shows 404 page
  }
  
  return (
    <article>
      <h1>{post.title}</h1>
      <time>{post.publishedAt}</time>
      <div>{post.content}</div>
    </article>
  );
}

// Generate static params for SSG
export async function generateStaticParams() {
  const posts = await db.post.findMany();
  
  return posts.map(post => ({
    slug: post.slug
  }));
}

// Generate metadata
export async function generateMetadata({ params }) {
  const post = await db.post.findUnique({
    where: { slug: params.slug }
  });
  
  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      images: [post.coverImage]
    }
  };
}

Example: Route handlers (API routes)

// app/api/users/route.ts - API Route Handler
import { NextResponse } from 'next/server';

// GET /api/users
export async function GET(request) {
  const { searchParams } = new URL(request.url);
  const page = searchParams.get('page') || '1';
  
  const users = await db.user.findMany({
    skip: (parseInt(page) - 1) * 10,
    take: 10
  });
  
  return NextResponse.json(users);
}

// POST /api/users
export async function POST(request) {
  const body = await request.json();
  
  const user = await db.user.create({
    data: body
  });
  
  return NextResponse.json(user, { status: 201 });
}

// app/api/users/[id]/route.ts - Dynamic route
export async function GET(request, { params }) {
  const user = await db.user.findUnique({
    where: { id: params.id }
  });
  
  if (!user) {
    return NextResponse.json({ error: 'Not found' }, { status: 404 });
  }
  
  return NextResponse.json(user);
}

// PATCH /api/users/[id]
export async function PATCH(request, { params }) {
  const body = await request.json();
  
  const user = await db.user.update({
    where: { id: params.id },
    data: body
  });
  
  return NextResponse.json(user);
}

// DELETE /api/users/[id]
export async function DELETE(request, { params }) {
  await db.user.delete({
    where: { id: params.id }
  });
  
  return NextResponse.json({ success: true });
}
Next.js App Router Features: File-based routing, Server Components by default, automatic code splitting, built-in loading states, error boundaries, metadata API, route handlers for APIs, streaming with Suspense, TypeScript support.

Server Components and Full-Stack React Best Practices