React Server Components Explained
Deep dive into React Server Components. Understand the architecture, benefits, and how to use them in your Next.js applications.
What are React Server Components?
React Server Components (RSC) are a new type of component that runs exclusively on the server, enabling better performance and simpler data fetching patterns.
Key Benefits
Zero Bundle Size
Server Components don’t add to your JavaScript bundle:
// ServerComponent.js - Runs only on server
import fs from 'fs';
export default async function ServerComponent() {
// This code never reaches the client
const data = await fs.promises.readFile('./data.json', 'utf-8');
const parsed = JSON.parse(data);
return (
<div>
{parsed.map(item => (
<Card key={item.id} data={item} />
))}
</div>
);
}
Direct Backend Access
Access databases and APIs directly without creating API routes:
// app/posts/page.jsx
import { db } from '@/lib/database';
export default async function PostsPage() {
// Direct database access
const posts = await db.post.findMany({
orderBy: { createdAt: 'desc' },
take: 10
});
return (
<div>
{posts.map(post => (
<PostCard key={post.id} post={post} />
))}
</div>
);
}
Automatic Code Splitting
Each Server Component is automatically split:
// Only the client components are sent to browser
export default async function Page() {
const data = await fetchData();
return (
<div>
<ServerHeader data={data} />
<ClientInteractive /> {/* Only this is in JS bundle */}
<ServerFooter />
</div>
);
}
Server vs Client Components
Server Components
// app/components/ServerComponent.jsx
// Default in Next.js 13+ App Router
export default async function ServerComponent() {
const data = await fetch('https://api.example.com/data');
return <div>{/* Server-rendered content */}</div>;
}
Features:
- Can be async
- Direct backend access
- Zero bundle size
- Cannot use hooks
- Cannot use browser APIs
- Cannot handle interactivity
Client Components
// app/components/ClientComponent.jsx
'use client'; // Opt-in to client rendering
import { useState, useEffect } from 'react';
export default function ClientComponent() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
}
Features:
- Can use hooks
- Can handle interactivity
- Can use browser APIs
- Adds to bundle size
- Cannot be async
- Cannot access backend directly
Composition Patterns
Server Components with Client Children
// ServerLayout.jsx
import ClientSidebar from './ClientSidebar';
export default async function ServerLayout({ children }) {
const user = await getUser();
return (
<div>
<ClientSidebar user={user} />
<main>{children}</main>
</div>
);
}
Passing Server Components to Client Components
// ClientWrapper.jsx
'use client';
export default function ClientWrapper({ children }) {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<button onClick={() => setIsOpen(!isOpen)}>Toggle</button>
{isOpen && children}
</div>
);
}
// Page.jsx (Server Component)
export default async function Page() {
return (
<ClientWrapper>
<ServerContent /> {/* Rendered on server */}
</ClientWrapper>
);
}
Data Fetching Patterns
Parallel Data Fetching
export default async function Page() {
// These fetch in parallel
const userPromise = fetchUser();
const postsPromise = fetchPosts();
const commentsPromise = fetchComments();
const [user, posts, comments] = await Promise.all([
userPromise,
postsPromise,
commentsPromise
]);
return <Dashboard user={user} posts={posts} comments={comments} />;
}
Streaming with Suspense
import { Suspense } from 'react';
export default function Page() {
return (
<div>
<Suspense fallback={<HeaderSkeleton />}>
<Header />
</Suspense>
<Suspense fallback={<ContentSkeleton />}>
<Content />
</Suspense>
<Suspense fallback={<SidebarSkeleton />}>
<Sidebar />
</Suspense>
</div>
);
}
async function Header() {
const data = await fetchHeaderData();
return <header>{data.title}</header>;
}
Sequential Data Fetching
export default async function Page({ params }) {
// Fetch user first
const user = await fetchUser(params.id);
// Then fetch their posts (depends on user data)
const posts = await fetchUserPosts(user.id);
return <Profile user={user} posts={posts} />;
}
Caching Strategies
Request Memoization
// This function is automatically memoized per request
async function getUser(id) {
const res = await fetch(`https://api.example.com/users/${id}`);
return res.json();
}
export default async function Page() {
// Both calls use the same cached result
const user1 = await getUser('123');
const user2 = await getUser('123'); // Cached!
return <div>{user1.name}</div>;
}
Data Cache
// Cache for 1 hour
export default async function Page() {
const data = await fetch('https://api.example.com/data', {
next: { revalidate: 3600 }
});
return <div>{data.title}</div>;
}
Revalidation
// app/actions.js
'use server';
import { revalidatePath, revalidateTag } from 'next/cache';
export async function createPost(formData) {
// Create post
await db.post.create({ data: formData });
// Revalidate the posts page
revalidatePath('/posts');
// Or revalidate by tag
revalidateTag('posts');
}
Best Practices
1. Keep Client Components Small
// Good - Minimal client component
'use client';
export function LikeButton({ postId }) {
const [liked, setLiked] = useState(false);
return (
<button onClick={() => setLiked(!liked)}>
{liked ? '❤️' : '🤍'}
</button>
);
}
// Use in Server Component
export default async function Post({ id }) {
const post = await getPost(id);
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
<LikeButton postId={id} />
</article>
);
}
2. Prefer Server Components
Use Server Components by default, opt into Client Components only when needed:
- User interactions (onClick, onChange)
- Browser APIs (localStorage, geolocation)
- React hooks (useState, useEffect)
- Custom hooks
- Class components
3. Use Suspense Boundaries
export default function Page() {
return (
<>
<Suspense fallback={<Skeleton />}>
<SlowComponent />
</Suspense>
<FastComponent /> {/* Shows immediately */}
</>
);
}
4. Optimize Images
import Image from 'next/image';
export default async function Gallery() {
const images = await getImages();
return (
<div>
{images.map(img => (
<Image
key={img.id}
src={img.url}
alt={img.alt}
width={800}
height={600}
loading="lazy"
/>
))}
</div>
);
}
Migration Tips
From Pages Router to App Router
// Before (Pages Router)
export async function getServerSideProps() {
const data = await fetchData();
return { props: { data } };
}
export default function Page({ data }) {
return <div>{data.title}</div>;
}
// After (App Router)
export default async function Page() {
const data = await fetchData();
return <div>{data.title}</div>;
}
Converting Client Components
// Identify what needs to be client-side
'use client';
import { useState } from 'react';
// Keep interactivity minimal
export function InteractiveSection() {
const [tab, setTab] = useState('overview');
return (
<div>
<Tabs value={tab} onChange={setTab} />
{tab === 'overview' && <Overview />}
{tab === 'details' && <Details />}
</div>
);
}
Common Patterns
Loading States
export default function Page() {
return (
<Suspense fallback={<Loading />}>
<DataComponent />
</Suspense>
);
}
Error Boundaries
// app/error.jsx
'use client';
export default function Error({ error, reset }) {
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={reset}>Try again</button>
</div>
);
}
Conclusion
React Server Components represent a fundamental shift in how we build React applications. By running components on the server, we can achieve better performance, simpler data fetching, and smaller bundle sizes. While there’s a learning curve, the benefits are substantial for most applications.
Start by building new features with Server Components, gradually migrate existing code, and always measure the impact on your application’s performance.