Back to blog

Server Components vs Client Components in Next.js

Key differences between Server and Client Components across Next.js versions 13, 14, and 15.

March 14, 2025
@berta.codes
14 min read
Server Components vs Client Components in Next.js

Next.js has revolutionized React development with its introduction of Server Components, fundamentally changing how we build web applications. This guide explores the differences between Server Components and Client Components in Next.js, with specific comparisons across versions 13, 14, and the latest version 15.

Understanding the Component Model Evolution #

The component model in Next.js has evolved significantly since the introduction of the App Router in version 13. Let's explore how Server and Client Components differ and what improvements versions 14 and 15 have brought to the table.

1. Rendering Location Server Components:

• Execute entirely on the server

• HTML is generated on the server and sent to the client

• No JavaScript bundle is sent to the client for these components

• Available in Next.js 13, 14, and 15, with performance improvements in each version

Client Components:

• Execute initially on the server for the first render (for hydration)

• Continue execution on the client browser

• Require JavaScript to be sent to the client

• Marked with the 'use client' directive at the top of the file

Version Differences: Next.js 13: Introduced the Server Components model with the App Router. Next.js 14: Server component rendering became up to 22% faster due to improvements in the React server module and optimized data fetching. Next.js 15: Integrates with React 19, providing further performance improvements and better hydration error handling with improved error messages that display the source code of the error with suggestions.

// Server Component (default in Next.js 13, 14, and 15)
// app/products/page.js
export default async function ProductsPage() {
  // This data fetching happens on the server
  const products = await fetch('https://api.example.com/products').then(res =>
    res.json()
  );

  return (
    <div>
      <h1>Products</h1>
      <ul>
        {products.map(product => (
          <li key={product.id}>{product.name}</li>
        ))}
      </ul>
    </div>
  );

}

// Client Component (Next.js 13, 14, and 15)
// app/counter.js
'use client';

import { useState } from 'react';

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

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

}

2. Data Fetching Patterns Server Components:

• Can fetch data directly in the component without useEffect or external libraries

• Data fetching doesn't block rendering in Next.js 14 and 15 (improved from 13)

• Support for parallel data fetching

Client Components:

• Require hooks like useEffect or SWR/React Query for data fetching

• Create additional client-server roundtrips

• Can lead to loading spinners and layout shifts

Version Differences: Next.js 13: Introduced basic data fetching in Server Components. Next.js 14: Introduced partial prerendering, which allows server components to stream in data without blocking the initial HTML response. Next.js 15: Changed caching semantics - GET Route Handlers and Client Router Cache are now uncached by default (previously cached by default). Also introduced async request APIs for headers, cookies, params, and searchParams.

// Next.js 15 Server Component with Async Request APIs
// app/dashboard/page.js
import { cookies, headers } from 'next/headers';

export default async function Dashboard({ searchParams }) {
  // In Next.js 15, these are now async APIs
  const cookieStore = await cookies();
  const headersList = await headers();

  // These fetch calls run in parallel automatically
  const userData = fetch('https://api.example.com/user').then(res =>
    res.json()
  );
  const revenueData = fetch('https://api.example.com/revenue').then(res =>
    res.json()
  );

  // Wait for both to resolve
  const [user, revenue] = await Promise.all([userData, revenueData]);

  return (
    <div>
      <h1>Welcome, {user.name}</h1>
      <RevenueChart data={revenue} />
    </div>
  );

}

// Client Component Data Fetching (consistent across versions)
// app/profile/client-profile.js
'use client';

import { useState, useEffect } from 'react';

export default function ClientProfile() {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    async function fetchUser() {
      try {
        const response = await fetch('https://api.example.com/user');
        if (!response.ok) {
          throw new Error('Failed to fetch user data');
        }
        const data = await response.json();
        setUser(data);
      } catch (err) {
        setError(err.message);
        console.error('Error fetching user:', err);
      } finally {
        setLoading(false);
      }
    }

    fetchUser();
  }, []);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  if (!user) return <div>No user data found</div>;

  return (
    <div>
      <h1>Profile: {user.name}</h1>
      <p>Email: {user.email}</p>
    </div>
  );

}

3. Interactivity and Event Handling Server Components:

• Cannot use React hooks (useState, useEffect, etc.)

• Cannot attach event handlers (onClick, onChange, etc.)

• Cannot access browser APIs (localStorage, window, etc.)

• Identical limitations across Next.js 13, 14, and 15

Client Components:

• Full access to React hooks ecosystem

• Can handle user interactions and events

• Can access browser APIs

• Behavior consistent across Next.js 13, 14, and 15

Version Differences: Next.js 13 & 14: Similar behavior for interactivity. Next.js 15: Integrates with React 19, which includes experimental support for the React Compiler. This compiler can automatically optimize your code by reducing the need for manual memoization with useMemo and useCallback, making Client Components more efficient.

// Server Component (cannot use hooks or event handlers)
export default function ServerButton() {
  // This would cause an error in Next.js 13, 14, and 15:
  // const [clicked, setClicked] = useState(false);

  // This would also cause an error:
  // return <button onClick={() => alert('Clicked')}>Click me</button>;

  // Instead, we need to use a Client Component for interactivity
  return <ClientButton />;

}

// Client Component for interactivity (Next.js 15 with React Compiler benefits)
'use client';

import { useState } from 'react';

export default function ClientButton() {
  const [clicked, setClicked] = useState(false);

  // In Next.js 15 with React Compiler (experimental),
  // this function would be automatically optimized without needing useCallback
  const handleClick = () => setClicked(!clicked);

  return (
    <button
      onClick={handleClick}
      className={clicked ? 'bg-green-500' : 'bg-blue-500'}
      aria-pressed={clicked}
    >
      {clicked ? 'Clicked!' : 'Click me'}
    </button>
  );

}

4. Bundle Size Impact Server Components:

• Zero impact on client JavaScript bundle size

• Don't contribute to the JavaScript that needs to be downloaded

• Improved performance for users on slow connections or mobile devices

Client Components:

• Increase the JavaScript bundle size

• Require hydration on the client

• Impact initial page load performance

Version Differences: Next.js 13: Introduced the concept of Server Components to reduce client bundle size. Next.js 14: Introduced improved tree-shaking and better code splitting, resulting in smaller client bundles. Next.js 15: Further optimized bundling of external packages, making client bundles even smaller and more efficient.

Component Composition Patterns #

The most effective Next.js applications use a hybrid approach, combining Server and Client Components strategically:

// app/products/[id]/page.js - Server Component
import { ProductDetails } from './product-details';
import { AddToCartButton } from './add-to-cart-button';
import { notFound } from 'next/navigation';

export default async function ProductPage({ params }) {
  // Fetch data on the server
  try {
    const response = await fetch(
      https://api.example.com/products/${params.id}
    );

    // Handle 404 errors gracefully
    if (!response.ok) {
      if (response.status === 404) {
        notFound();
      }
      throw new Error(Failed to fetch product: ${response.statusText});
    }

    const product = await response.json();

    return (
      <div className="product-page">
        {/<em> Server Component with product data </em>/}
        <ProductDetails product={product} />

        {/<em> Client Component for interactivity </em>/}
        <AddToCartButton productId={product.id} />
      </div>
    );
  } catch (error) {
    console.error('Error loading product:', error);
    throw error; // Let the error boundary handle it
  }

}

// app/products/[id]/product-details.js - Server Component
export function ProductDetails({ product }) {
  if (!product) return null;

  return (
    <div>
      <h1>{product.name}</h1>
      <p className="description">{product.description}</p>
      <div className="price">${product.price.toFixed(2)}</div>
    </div>
  );

}

// app/products/[id]/add-to-cart-button.js - Client Component
'use client';

import { useState } from 'react';

export function AddToCartButton({ productId }) {
  const [isAdding, setIsAdding] = useState(false);
  const [error, setError] = useState(null);
  const [success, setSuccess] = useState(false);

  async function handleAddToCart() {
    setIsAdding(true);
    setError(null);
    setSuccess(false);

    try {
      const response = await fetch('/api/cart', {
        method: 'POST',
        body: JSON.stringify({ productId }),
        headers: { 'Content-Type': 'application/json' },
      });

      if (!response.ok) {
        throw new Error('Failed to add item to cart');
      }

      setSuccess(true);
    } catch (err) {
      setError(err.message);
      console.error('Error adding to cart:', err);
    } finally {
      setIsAdding(false);
    }
  }

  return (
    <div>
      <button
        onClick={handleAddToCart}
        disabled={isAdding}
        className={add-to-cart-button ${success ? 'success' : ''} ${error ? 'error' : ''}}
        aria-busy={isAdding}
      >
        {isAdding ? 'Adding...' : success ? 'Added to Cart' : 'Add to Cart'}
      </button>

      {error && <p className="error-message">{error}</p>}
    </div>
  );

}

6. Form Handling and Server Actions Server Actions:

• Allow Server Components to handle form submissions and data mutations

• Enable progressive enhancement with and without JavaScript

Version Differences: Next.js 13: Server Actions were in experimental stage. Next.js 14: Server Actions became stable and production-ready with seamless form integration. Next.js 15: Introduced the new
component that extends the HTML element with prefetching, client-side navigation, and progressive enhancement. Also added enhanced security for Server Actions.

// Next.js 15 Form Component with Server Action
// app/contact/page.js
import Form from 'next/form';
import { redirect } from 'next/navigation';

export default function ContactPage() {
  async function submitForm(formData) {
    'use server'; // This marks the function as a Server Action

    const name = formData.get('name');
    const email = formData.get('email');
    const message = formData.get('message');

    // Server-side validation
    const errors = {};
    if (!name) errors.name = 'Name is required';
    if (!email) errors.email = 'Email is required';
    if (email && !/^\S+@\S+\.\S+$/.test(email))
      errors.email = 'Valid email is required';
    if (!message) errors.message = 'Message is required';

    if (Object.keys(errors).length > 0) {
      return { errors };
    }

    try {
      // Process the form data (e.g., send email, save to database)
      await saveMessageToDatabase({ name, email, message });

      // Redirect on success
      redirect('/contact/success');
    } catch (error) {
      console.error('Error saving message:', error);
      return {
        serverError: 'Failed to send message. Please try again later.',
      };
    }
  }

  return (
    <div>
      <h1>Contact Us</h1>
      <Form action={submitForm}>
        {({ pending, data }) => (
          <>
            {data?.serverError && (
              <div className="error-message">{data.serverError}</div>
            )}

            <div className="form-group">
              <label htmlFor="name">Name</label>
              <input type="text" id="name" name="name" required />
              {data?.errors?.name && (
                <div className="field-error">{data.errors.name}</div>
              )}
            </div>

            <div className="form-group">
              <label htmlFor="email">Email</label>
              <input type="email" id="email" name="email" required />
              {data?.errors?.email && (
                <div className="field-error">{data.errors.email}</div>
              )}
            </div>

            <div className="form-group">
              <label htmlFor="message">Message</label>
              <textarea id="message" name="message" required></textarea>
              {data?.errors?.message && (
                <div className="field-error">{data.errors.message}</div>
              )}
            </div>

            <button type="submit" disabled={pending}>
              {pending ? 'Sending...' : 'Send Message'}
            </button>
          </>
        )}
      </Form>
    </div>
  );

}

7. Error Handling Server Components:

• Errors can be caught using error.js boundary files

• More predictable error handling since execution happens in a controlled server environment

Client Components:

• Require try/catch blocks or error boundaries

• Errors can affect the user experience more directly

Version Differences: Next.js 13: Basic error handling with error.js boundaries. Next.js 14: Improved error recovery mechanisms and better integration between Server and Client error boundaries. Next.js 15: Significantly enhanced hydration error messages that display the source code of the error with specific suggestions on how to address the issue.

// error.js - Error Boundary for Server Components (Next.js 15)
'use client'; // Error boundaries must be Client Components

import { useEffect } from 'react';

export default function Error({ error, reset }) {
  useEffect(() => {
    // Log the error to an error reporting service
    console.error('Application error:', error);
  }, [error]);

  return (
    <div className="error-container">
      <h2>Something went wrong!</h2>
      <p>{error.message || 'An unexpected error occurred'}</p>
      <button onClick={() => reset()} className="reset-button">
        Try again
      </button>
    </div>
  );

}

8. SEO and Metadata Server Components:

• Ideal for SEO as they generate HTML on the server

• Can use the Metadata API for dynamic metadata

• Content is immediately available to search engine crawlers

Client Components:

• Less optimal for SEO as content may not be available during initial crawl

• Require additional considerations for SEO optimization

Version Differences: Next.js 13: Introduced the Metadata API. Next.js 14: Expanded the Metadata API capabilities. Next.js 15: Made the Metadata API more flexible with async request APIs for params in generateMetadata.

// app/products/[id]/page.js (Next.js 15)
import { Metadata } from 'next';
import { notFound } from 'next/navigation';

// Dynamic metadata generation in Next.js 15 with async params
export async function generateMetadata({ params }) {
  // In Next.js 15, params is now an async API
  const id = params.id;

  try {
    const response = await fetch(https://api.example.com/products/${id});

    if (!response.ok) {
      if (response.status === 404) {
        return {
          title: 'Product Not Found | My Store',
          description: 'The requested product could not be found.',
        };
      }
      throw new Error(
        Failed to fetch product metadata: ${response.statusText}
      );
    }

    const product = await response.json();

    return {
      title: ${product.name} | My Store,
      description: product.description.substring(0, 160), // Limit description length for SEO
      openGraph: {
        title: product.name,
        description: product.description.substring(0, 160),
        images: [
          {
            url: product.imageUrl,
            width: 1200,
            height: 630,
            alt: product.name,
          },
        ],
        type: 'website',
      },
      twitter: {
        card: 'summary_large_image',
        title: product.name,
        description: product.description.substring(0, 160),
        images: [product.imageUrl],
      },
    };
  } catch (error) {
    console.error('Error generating metadata:', error);
    return {
      title: 'Product | My Store',
      description: 'View our product details',
    };
  }
}

export default function ProductPage({ params }) {
  // Component implementation

}

9. Development Experience Server Components:

• Errors are displayed in both terminal and browser

• Debugging requires server-side tools

• Fast Refresh works differently than with Client Components

Client Components:

• Familiar debugging experience using browser DevTools

• Errors appear in browser console

• Traditional React debugging approaches apply

Version Differences: Next.js 13: Basic development experience for Server Components. Next.js 14: Improved error messages for Server Components. Next.js 15: Introduced the Static Route Indicator during development to help identify which routes are static or dynamic, making it easier to optimize performance. Also made Turbopack stable with the next dev --turbo command, providing up to 76.7% faster local server startup and 96.3% faster code updates with Fast Refresh. 10. Post-Response Code Execution Next.js 15 Exclusive Feature:

Next.js 15 introduced the experimental unstable_after API, which allows you to schedule work to be processed after the response has finished streaming. This enables secondary tasks like logging and analytics to run without blocking the primary response.

// Next.js 15 unstable_after API
// app/layout.js
import { unstable_after as after } from 'next/server';
import { logPageView } from '@/lib/analytics';

export default function Layout({ children }) {
  // Secondary task that won't block the response
  after(() => {
    try {
      logPageView();
    } catch (error) {
      // Ensure errors in the after callback don't affect the user
      console.error('Error in after callback:', error);
    }
  });

  // Primary task - renders immediately
  return <>{children}</>;

}

TypeScript Configuration Support #

Next.js 15 Exclusive Feature:

Next.js 15 added support for TypeScript configuration files with next.config.ts, providing type-safe options and better developer experience.

// next.config.ts in Next.js 15
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
  // Type-safe configuration options
  reactStrictMode: true,
  images: {
    domains: ['example.com'],
    formats: ['image/avif', 'image/webp'],
  },
  experimental: {
    serverActions: {
      allowedOrigins: ['mydomain.com', '*.mydomain.com'],
    },
  },
};

export default nextConfig;

When to Use Each Component Type #

Use Server Components When:

• Fetching data

• Accessing backend resources directly

• Keeping sensitive information on the server

• Displaying static or infrequently updated content

• Reducing client-side JavaScript

• Implementing SEO-critical pages

Use Client Components When:

• Adding interactivity and event listeners

• Using React hooks (useState, useEffect, etc.)

• Accessing browser APIs

• Using custom hooks that depend on state or effects

• Implementing client-side form validation

• Creating interactive UI elements like modals, dropdowns, and tabs

Conclusion #

The Server Components vs Client Components paradigm represents a fundamental shift in how we build React applications. Next.js 13 introduced this revolutionary approach, version 14 refined it with better performance and stable Server Actions, and version 15 has further enhanced it with React 19 integration, improved caching semantics, and new features like the component and unstable_after API.

By understanding when and how to use each component type across these versions, you can create applications that offer the best of both worlds: the performance and SEO benefits of server rendering with the rich interactivity of client-side React.

The future of React development is hybrid, and Next.js continues to lead the way with its innovative component model that evolves with each new version.

Share this post

This website uses cookies to analyze traffic and enhance your experience. By clicking "Accept", you consent to our use of cookies for analytics purposes. You can withdraw your consent at any time by changing your browser settings. Cookie Policy