Skip to main content

Next.js Server Components

Introduction

Server Components represent one of the most significant paradigm shifts in React's history. Introduced as a core feature in Next.js 13 and the App Router, Server Components allow developers to render components on the server, reducing the JavaScript sent to the client and improving application performance.

In this guide, we'll explore what Server Components are, how they work in Next.js, and how to use them effectively for data fetching and rendering.

What Are Server Components?

Server Components are React components that render exclusively on the server. Unlike traditional React components that run in the browser, Server Components:

  • Execute only on the server
  • Don't send JavaScript to the client
  • Can directly access server resources (databases, file systems)
  • Reduce the client-side JavaScript bundle size

In Next.js, all components inside the App Router are Server Components by default, which represents a significant shift from the previous Pages Router architecture.

Server vs. Client Components

To understand Server Components better, let's compare them with Client Components:

FeatureServer ComponentsClient Components
Rendering locationServerClient (browser)
JavaScript sent to clientNoneComponent code
Access to server resourcesDirect accessNone (requires API)
Can use React hooksNoYes
Can be interactiveNoYes
Default in Next.js App RouterYesNo (must opt-in)

Using Server Components for Data Fetching

One of the biggest advantages of Server Components is their ability to fetch data directly from data sources without needing an API layer.

Basic Data Fetching Example

Here's how you can fetch data directly in a Server Component:

jsx
// app/posts/page.js
async function getPosts() {
// This function runs on the server only
const res = await fetch('https://jsonplaceholder.typicode.com/posts');

if (!res.ok) {
throw new Error('Failed to fetch posts');
}

return res.json();
}

export default async function PostsPage() {
// This component is a Server Component by default
const posts = await getPosts();

return (
<div>
<h1>Posts</h1>
<ul>
{posts.map((post) => (
<li key={post.id}>
<h2>{post.title}</h2>
<p>{post.body}</p>
</li>
))}
</ul>
</div>
);
}

Notice how we can:

  • Use async/await directly in our components
  • Await data before rendering the component
  • Don't need to use hooks like useEffect or state management

Database Access with Server Components

Server Components can connect directly to databases, eliminating the need for an API middleware:

jsx
// app/dashboard/page.js
import { sql } from '@vercel/postgres';

export default async function Dashboard() {
// Direct database query on the server
const { rows } = await sql`SELECT * FROM users`;

return (
<div>
<h1>Dashboard</h1>
<h2>Users</h2>
<ul>
{rows.map((user) => (
<li key={user.id}>{user.name} - {user.email}</li>
))}
</ul>
</div>
);
}

Creating Interactive UI with Server and Client Components

While Server Components are powerful, they can't handle interactivity on their own. For interactive elements, you'll need to use Client Components.

Client Components in Next.js

To create a Client Component, add the 'use client' directive at the top of your file:

jsx
'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>
);
}

Composing Server and Client Components

You can compose Server and Client Components together. A common pattern is to fetch data in a Server Component and pass it to a Client Component:

jsx
// app/posts/page.js - Server Component
async function getPosts() {
const res = await fetch('https://jsonplaceholder.typicode.com/posts');
return res.json();
}

import PostList from './post-list'; // Client Component

export default async function PostsPage() {
const posts = await getPosts();

return (
<div>
<h1>Posts</h1>
<PostList initialPosts={posts} />
</div>
);
}
jsx
// app/posts/post-list.js - Client Component
'use client'

import { useState } from 'react';

export default function PostList({ initialPosts }) {
const [posts, setPosts] = useState(initialPosts);
const [filter, setFilter] = useState('');

const filteredPosts = posts.filter(post =>
post.title.toLowerCase().includes(filter.toLowerCase())
);

return (
<div>
<input
type="text"
placeholder="Filter posts..."
value={filter}
onChange={(e) => setFilter(e.target.value)}
/>

<ul>
{filteredPosts.map((post) => (
<li key={post.id}>
<h2>{post.title}</h2>
<p>{post.body}</p>
</li>
))}
</ul>
</div>
);
}

Patterns and Best Practices

When to Use Server Components

Use Server Components for:

  • Data fetching
  • Accessing backend resources
  • Keeping sensitive information on the server
  • Large dependencies that shouldn't be sent to the client
  • SEO and static content

When to Use Client Components

Use Client Components for:

  • Interactivity and event listeners
  • Using hooks like useState, useEffect, etc.
  • Browser-only APIs
  • Custom event handlers
  • Component lifecycle effects

Moving Component Logic to the Server

A major benefit of Server Components is moving heavy computation to the server:

jsx
// app/analytics/page.js
import { calculateAnalytics } from '@/lib/analytics';

export default async function AnalyticsPage() {
// Heavy computation runs on the server
const { totalUsers, activeUsers, revenue } = await calculateAnalytics();

return (
<div>
<h1>Analytics Dashboard</h1>
<div className="stats">
<div className="stat-card">
<h2>Total Users</h2>
<p className="number">{totalUsers.toLocaleString()}</p>
</div>
<div className="stat-card">
<h2>Active Users</h2>
<p className="number">{activeUsers.toLocaleString()}</p>
</div>
<div className="stat-card">
<h2>Revenue</h2>
<p className="number">${revenue.toLocaleString()}</p>
</div>
</div>
</div>
);
}

Real-World Example: A Blog with Server Components

Let's build a simplified blog using Server Components for data fetching and Client Components for interactivity:

jsx
// app/blog/page.js - Server Component (main page)
import { getBlogPosts } from '@/lib/blog-data';
import BlogPostList from './blog-post-list';
import RecentComments from './recent-comments';

export default async function BlogPage() {
const posts = await getBlogPosts();

return (
<div className="blog-container">
<h1>Our Blog</h1>
<div className="blog-layout">
<main>
<BlogPostList posts={posts} />
</main>
<aside>
<RecentComments />
</aside>
</div>
</div>
);
}
jsx
// app/blog/blog-post-list.js - Client Component
'use client'

import { useState } from 'react';
import Link from 'next/link';

export default function BlogPostList({ posts }) {
const [category, setCategory] = useState('all');

const filteredPosts = category === 'all'
? posts
: posts.filter(post => post.category === category);

return (
<div className="post-list">
<div className="filter-controls">
<button
className={category === 'all' ? 'active' : ''}
onClick={() => setCategory('all')}
>
All
</button>
<button
className={category === 'technology' ? 'active' : ''}
onClick={() => setCategory('technology')}
>
Technology
</button>
<button
className={category === 'lifestyle' ? 'active' : ''}
onClick={() => setCategory('lifestyle')}
>
Lifestyle
</button>
</div>

{filteredPosts.map(post => (
<article key={post.id} className="post-card">
<h2>
<Link href={`/blog/${post.slug}`}>
{post.title}
</Link>
</h2>
<p className="post-excerpt">{post.excerpt}</p>
<div className="post-meta">
<span>{post.date}</span>
<span className="category">{post.category}</span>
</div>
</article>
))}
</div>
);
}
jsx
// app/blog/recent-comments.js - Server Component
import { getRecentComments } from '@/lib/comment-data';

export default async function RecentComments() {
const comments = await getRecentComments();

return (
<div className="comments-sidebar">
<h3>Recent Comments</h3>
<ul className="comment-list">
{comments.map(comment => (
<li key={comment.id} className="comment-item">
<p className="comment-text">"{comment.text}"</p>
<p className="comment-author">- {comment.author}</p>
</li>
))}
</ul>
</div>
);
}

Advanced Patterns

Streaming with Suspense

Next.js supports streaming UI with Suspense for gradually sending UI from the server to the client:

jsx
// app/dashboard/page.js
import { Suspense } from 'react';
import UserProfile from './user-profile';
import LatestActivity from './latest-activity';
import PerformanceMetrics from './performance-metrics';

export default function Dashboard() {
return (
<div className="dashboard">
<h1>Dashboard</h1>

<div className="dashboard-layout">
{/* This loads immediately */}
<Suspense fallback={<div className="loading">Loading profile...</div>}>
<UserProfile />
</Suspense>

{/* This might take longer but won't block the profile */}
<Suspense fallback={<div className="loading">Loading activity...</div>}>
<LatestActivity />
</Suspense>

{/* Complex metrics that take even longer */}
<Suspense fallback={<div className="loading">Loading metrics...</div>}>
<PerformanceMetrics />
</Suspense>
</div>
</div>
);
}

Selective Hydration

With Next.js Server Components, the server sends HTML with selective hydration, prioritizing interactive parts that need JavaScript:

jsx
// app/product/[id]/page.js
import ProductImage from './product-image'; // Server Component
import ProductDetails from './product-details'; // Server Component
import AddToCartButton from './add-to-cart-button'; // Client Component
import ProductReviews from './product-reviews'; // Client Component

export default async function ProductPage({ params }) {
const { id } = params;
const product = await getProduct(id);

return (
<div className="product-page">
<div className="product-showcase">
{/* Rendered as static HTML, no JS needed */}
<ProductImage src={product.image} alt={product.name} />

<div className="product-info">
{/* Rendered as static HTML, no JS needed */}
<ProductDetails product={product} />

{/* Only this will be hydrated with JS */}
<AddToCartButton productId={product.id} />
</div>
</div>

{/* This can hydrate independently */}
<ProductReviews productId={product.id} />
</div>
);
}

Common Pitfalls and Solutions

Mistakenly Using Client Features in Server Components

jsx
// ❌ This won't work in a Server Component
export default function Broken() {
const [count, setCount] = useState(0);

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

Solution: Move interactive logic to a Client Component:

jsx
// app/components/counter.js
'use client'

import { useState } from 'react';

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

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

// app/page.js
import Counter from './components/counter';

export default function Page() {
return (
<div>
<h1>My Page</h1>
<Counter />
</div>
);
}

Accessing Browser APIs on the Server

jsx
// ❌ This will fail on the server
export default function ErrorComponent() {
// This runs on the server where window doesn't exist
const width = window.innerWidth;

return <p>Window width is: {width}px</p>;
}

Solution: Use a Client Component with useEffect:

jsx
// app/components/window-size.js
'use client'

import { useState, useEffect } from 'react';

export default function WindowSize() {
const [width, setWidth] = useState(0);

useEffect(() => {
const handleResize = () => setWidth(window.innerWidth);
setWidth(window.innerWidth);
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);

return <p>Window width is: {width}px</p>;
}

Summary

Server Components in Next.js represent a fundamental shift in how we build React applications. By moving rendering and data fetching to the server, we can:

  1. Improve performance by reducing client-side JavaScript
  2. Simplify data fetching by accessing server resources directly
  3. Enhance user experience through faster page loads and progressive rendering
  4. Optimize SEO with server-rendered content
  5. Reduce complexity by eliminating API routes for data that's only used for rendering

The combination of Server Components and Client Components allows developers to create applications that are both interactive and performant, leveraging the best of both server and client rendering.

Additional Resources

Exercises

  1. Convert an existing component that fetches data client-side to use Server Components.
  2. Create a blog application that uses Server Components for rendering posts and Client Components for comments and interactions.
  3. Build a dashboard with multiple data sections using Suspense boundaries for progressive loading.
  4. Implement direct database access in a Server Component and pass the data to a Client Component.
  5. Create a hybrid application with both Server and Client Components that optimizes for performance while maintaining interactivity.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)