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:
Feature | Server Components | Client Components |
---|---|---|
Rendering location | Server | Client (browser) |
JavaScript sent to client | None | Component code |
Access to server resources | Direct access | None (requires API) |
Can use React hooks | No | Yes |
Can be interactive | No | Yes |
Default in Next.js App Router | Yes | No (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:
// 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:
// 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:
'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:
// 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>
);
}
// 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:
// 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:
// 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>
);
}
// 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>
);
}
// 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:
// 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:
// 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
// ❌ 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:
// 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
// ❌ 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:
// 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:
- Improve performance by reducing client-side JavaScript
- Simplify data fetching by accessing server resources directly
- Enhance user experience through faster page loads and progressive rendering
- Optimize SEO with server-rendered content
- 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
- Next.js Documentation on Server Components
- React Server Components RFC
- Data Fetching Fundamentals in Next.js
- Vercel's Guide to Server Components
Exercises
- Convert an existing component that fetches data client-side to use Server Components.
- Create a blog application that uses Server Components for rendering posts and Client Components for comments and interactions.
- Build a dashboard with multiple data sections using Suspense boundaries for progressive loading.
- Implement direct database access in a Server Component and pass the data to a Client Component.
- 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! :)