React Server Components are a revolutionary React feature that allows rendering components on the server side while maintaining interactivity on the client side. This is a fundamental change in how we think about React applications.
React Server Components are components that are rendered on the server side and send ready HTML to the client along with instructions for React on how to integrate that HTML with interactive client components.
1// Traditional SSR - entire application rendered on server
2function TraditionalSSRPage() {
3 const [count, setCount] = useState(0);
4 const posts = await fetchPosts(); // All logic must be on client after hydration
5
6 return (
7 <div>
8 <PostsList posts={posts} />
9 <Counter count={count} setCount={setCount} />
10 </div>
11 );
12}
13
14// React Server Components - server and client mix
15async function ServerComponentPage() {
16 // This code runs ONLY on the server
17 const posts = await fetchPosts(); // Direct database call
18 const user = await getUser();
19
20 return (
21 <div>
22 {/* Server Component - rendered on server */}
23 <PostsList posts={posts} />
24
25 {/* Client Component - interactive on client */}
26 <Counter />
27
28 {/* Server Component with server data */}
29 <UserProfile user={user} />
30 </div>
31 );
32}1// ServerComponent.jsx (server component by default)
2import { db } from './database';
3
4export default async function PostsList() {
5 // Direct database access
6 const posts = await db.posts.findMany({
7 include: { author: true },
8 orderBy: { createdAt: 'desc' }
9 });
10
11 return (
12 <div className="posts-list">
13 {posts.map(post => (
14 <article key={post.id}>
15 <h2>{post.title}</h2>
16 <p>By {post.author.name}</p>
17 <p>{post.excerpt}</p>
18
19 {/* Client Component for interaction */}
20 <LikeButton postId={post.id} initialLikes={post.likes} />
21 </article>
22 ))}
23 </div>
24 );
25}
26
27// LikeButton.jsx (client component)
28'use client'; // Directive marking a client component
29
30import { useState } from 'react';
31
32export default function LikeButton({ postId, initialLikes }) {
33 const [likes, setLikes] = useState(initialLikes);
34 const [isLiking, setIsLiking] = useState(false);
35
36 const handleLike = async () => {
37 setIsLiking(true);
38 try {
39 const response = await fetch(`/api/posts/${postId}/like`, {
40 method: 'POST'
41 });
42 const data = await response.json();
43 setLikes(data.likes);
44 } catch (error) {
45 console.error('Failed to like post:', error);
46 } finally {
47 setIsLiking(false);
48 }
49 };
50
51 return (
52 <button
53 onClick={handleLike}
54 disabled={isLiking}
55 className="like-button"
56 >
57 ❤️ {likes} {isLiking && '...'}
58 </button>
59 );
60}1// utils/api.js (server-side only)
2import { cache } from 'react';
3
4// Automatic caching for the same request
5export const getPosts = cache(async () => {
6 const response = await fetch('https://api.example.com/posts', {
7 // Server Components can safely use secrets
8 headers: {
9 'Authorization': `Bearer ${process.env.API_SECRET}`
10 }
11 });
12 return response.json();
13});
14
15export const getUser = cache(async (id) => {
16 // Direct database query
17 return await db.user.findUnique({
18 where: { id },
19 include: { profile: true }
20 });
21});
22
23// components/PostsPage.jsx
24import { getPosts, getUser } from '../utils/api';
25import { Suspense } from 'react';
26
27export default async function PostsPage({ userId }) {
28 // Parallel data fetching
29 const [posts, user] = await Promise.all([
30 getPosts(),
31 getUser(userId)
32 ]);
33
34 return (
35 <main>
36 <h1>Welcome, {user.name}!</h1>
37
38 {/* Nested Server Component with its own data fetching */}
39 <Suspense fallback={<div>Loading posts...</div>}>
40 <PostsList posts={posts} />
41 </Suspense>
42
43 {/* Client Component for interaction */}
44 <CreatePostForm userId={userId} />
45 </main>
46 );
47}1// components/DashboardPage.jsx
2import { Suspense } from 'react';
3
4export default function DashboardPage() {
5 return (
6 <div className="dashboard">
7 <h1>Dashboard</h1>
8
9 {/* Fast loading content */}
10 <Suspense fallback={<SkeletonCard />}>
11 <UserStats />
12 </Suspense>
13
14 {/* Slower loading content */}
15 <Suspense fallback={<SkeletonChart />}>
16 <AnalyticsChart />
17 </Suspense>
18
19 {/* Very slow loading content */}
20 <Suspense fallback={<SkeletonTable />}>
21 <ReportsTable />
22 </Suspense>
23 </div>
24 );
25}
26
27// components/UserStats.jsx (Server Component)
28async function UserStats() {
29 // Fast query - cache hit
30 const stats = await getUserStats();
31
32 return (
33 <div className="stats-grid">
34 <StatCard title="Total Users" value={stats.totalUsers} />
35 <StatCard title="Active Today" value={stats.activeToday} />
36 <StatCard title="Revenue" value={`$${stats.revenue}`} />
37 </div>
38 );
39}
40
41// components/AnalyticsChart.jsx (Server Component)
42async function AnalyticsChart() {
43 // Medium-length query
44 const chartData = await getAnalyticsData();
45
46 return (
47 <div className="chart-container">
48 {/* Client Component for interactive chart */}
49 <InteractiveChart data={chartData} />
50 </div>
51 );
52}
53
54// components/ReportsTable.jsx (Server Component)
55async function ReportsTable() {
56 // Long query - complex aggregations
57 const reports = await getComplexReports();
58
59 return (
60 <div className="reports-table">
61 <table>
62 <thead>
63 <tr>
64 <th>Report</th>
65 <th>Status</th>
66 <th>Date</th>
67 </tr>
68 </thead>
69 <tbody>
70 {reports.map(report => (
71 <tr key={report.id}>
72 <td>{report.name}</td>
73 <td>{report.status}</td>
74 <td>{report.date}</td>
75 </tr>
76 ))}
77 </tbody>
78 </table>
79 </div>
80 );
81}1// patterns/CompositionPatterns.jsx
2
3// GOOD - Server Component can render Client Component
4function ServerParent() {
5 const data = await fetchData();
6
7 return (
8 <div>
9 <h1>Server Rendered Title</h1>
10 <ClientChild data={data} />
11 </div>
12 );
13}
14
15// GOOD - Client Component can render Server Component via children
16'use client';
17function ClientWrapper({ children }) {
18 const [isOpen, setIsOpen] = useState(false);
19
20 return (
21 <div>
22 <button onClick={() => setIsOpen(!isOpen)}>
23 Toggle Content
24 </button>
25 {isOpen && children}
26 </div>
27 );
28}
29
30// Usage:
31function ParentPage() {
32 return (
33 <ClientWrapper>
34 <ServerContent /> {/* Server Component as children */}
35 </ClientWrapper>
36 );
37}
38
39// BAD - Client Component cannot directly import Server Component
40'use client';
41function BadClientComponent() {
42 const [show, setShow] = useState(false);
43
44 return (
45 <div>
46 {show && <ServerContent />} {/* This won't work! */}
47 </div>
48 );
49}
50
51// GOOD - Using render props pattern
52'use client';
53function GoodClientComponent({ renderContent }) {
54 const [show, setShow] = useState(false);
55
56 return (
57 <div>
58 <button onClick={() => setShow(!show)}>Toggle</button>
59 {show && renderContent()}
60 </div>
61 );
62}
63
64// Usage:
65function ParentComponent() {
66 return (
67 <GoodClientComponent
68 renderContent={() => <ServerContent />}
69 />
70 );
71}1// actions/postActions.js
2'use server'; // Server Actions
3
4import { revalidatePath } from 'next/cache';
5
6export async function createPost(formData) {
7 const title = formData.get('title');
8 const content = formData.get('content');
9
10 // Server-side validation
11 if (!title || !content) {
12 return { error: 'Title and content are required' };
13 }
14
15 try {
16 const post = await db.post.create({
17 data: { title, content, userId: getCurrentUserId() }
18 });
19
20 // Revalidate cache for the posts page
21 revalidatePath('/posts');
22
23 return { success: true, post };
24 } catch (error) {
25 return { error: 'Failed to create post' };
26 }
27}
28
29export async function deletePost(postId) {
30 try {
31 await db.post.delete({
32 where: { id: postId }
33 });
34
35 revalidatePath('/posts');
36 return { success: true };
37 } catch (error) {
38 return { error: 'Failed to delete post' };
39 }
40}
41
42// components/CreatePostForm.jsx
43'use client';
44
45import { createPost } from '../actions/postActions';
46import { useFormStatus } from 'react-dom';
47
48function SubmitButton() {
49 const { pending } = useFormStatus();
50
51 return (
52 <button type="submit" disabled={pending}>
53 {pending ? 'Creating...' : 'Create Post'}
54 </button>
55 );
56}
57
58export default function CreatePostForm() {
59 const [state, formAction] = useActionState(createPost, null);
60
61 return (
62 <form action={formAction}>
63 <div>
64 <label htmlFor="title">Title:</label>
65 <input
66 type="text"
67 id="title"
68 name="title"
69 required
70 />
71 </div>
72
73 <div>
74 <label htmlFor="content">Content:</label>
75 <textarea
76 id="content"
77 name="content"
78 required
79 />
80 </div>
81
82 {state?.error && (
83 <div className="error">{state.error}</div>
84 )}
85
86 {state?.success && (
87 <div className="success">Post created successfully!</div>
88 )}
89
90 <SubmitButton />
91 </form>
92 );
93}1// utils/cache.js
2import { unstable_cache } from 'next/cache';
3
4// Custom cache wrapper with tags
5export function createCachedFunction(fn, keyParts, options = {}) {
6 return unstable_cache(
7 fn,
8 keyParts,
9 {
10 revalidate: 3600, // 1 hour default
11 tags: options.tags || [],
12 ...options
13 }
14 );
15}
16
17// Cached functions
18export const getCachedPosts = createCachedFunction(
19 async () => {
20 return await db.post.findMany({
21 include: { author: true },
22 orderBy: { createdAt: 'desc' }
23 });
24 },
25 ['posts'],
26 {
27 tags: ['posts'],
28 revalidate: 300 // 5 minutes
29 }
30);
31
32export const getCachedUser = createCachedFunction(
33 async (userId) => {
34 return await db.user.findUnique({
35 where: { id: userId },
36 include: { profile: true }
37 });
38 },
39 ['user'],
40 {
41 tags: ['users'],
42 revalidate: 3600 // 1 hour
43 }
44);
45
46// components/PostsWithCache.jsx
47export default async function PostsWithCache() {
48 // Automatic caching
49 const posts = await getCachedPosts();
50
51 return (
52 <div>
53 <h1>Cached Posts</h1>
54 {posts.map(post => (
55 <article key={post.id}>
56 <h2>{post.title}</h2>
57 <p>By {post.author.name}</p>
58 <CacheRevalidateButton postId={post.id} />
59 </article>
60 ))}
61 </div>
62 );
63}
64
65// actions/cacheActions.js
66'use server';
67
68import { revalidateTag } from 'next/cache';
69
70export async function revalidatePosts() {
71 revalidateTag('posts');
72}
73
74export async function revalidateUsers() {
75 revalidateTag('users');
76}1// Server Components don't end up in the client bundle!
2
3// ServerHeavyComponent.jsx (Server Component)
4import { someHeavyLibrary } from 'heavy-library'; // Won't be in client bundle!
5import { complexCalculation } from './utils/complex';
6
7export default async function ServerHeavyComponent() {
8 const data = await fetchData();
9 const processed = complexCalculation(data); // Happens on server
10 const result = someHeavyLibrary.process(processed);
11
12 return (
13 <div>
14 <h2>Processed Data</h2>
15 <pre>{JSON.stringify(result, null, 2)}</pre>
16 </div>
17 );
18}
19
20// ClientLightComponent.jsx (Client Component)
21'use client';
22
23import { useState } from 'react';
24
25export default function ClientLightComponent({ serverData }) {
26 const [selected, setSelected] = useState(null);
27
28 return (
29 <div>
30 {serverData.map(item => (
31 <button
32 key={item.id}
33 onClick={() => setSelected(item)}
34 className={selected?.id === item.id ? 'selected' : ''}
35 >
36 {item.name}
37 </button>
38 ))}
39 {selected && <ItemDetails item={selected} />}
40 </div>
41 );
42}1// components/ServerErrorBoundary.jsx
2'use client';
3
4import { Component } from 'react';
5
6export class ServerErrorBoundary extends Component {
7 constructor(props) {
8 super(props);
9 this.state = { hasError: false, error: null };
10 }
11
12 static getDerivedStateFromError(error) {
13 return { hasError: true, error };
14 }
15
16 componentDidCatch(error, errorInfo) {
17 console.error('Server component error:', error, errorInfo);
18 // Send to monitoring service
19 }
20
21 render() {
22 if (this.state.hasError) {
23 return (
24 <div className="error-boundary">
25 <h2>Something went wrong on the server</h2>
26 <details>
27 <summary>Error details</summary>
28 <pre>{this.state.error?.message}</pre>
29 </details>
30 <button
31 onClick={() => this.setState({ hasError: false })}
32 >
33 Try again
34 </button>
35 </div>
36 );
37 }
38
39 return this.props.children;
40 }
41}
42
43// Usage in layout
44export default function Layout({ children }) {
45 return (
46 <html>
47 <body>
48 <ServerErrorBoundary>
49 {children}
50 </ServerErrorBoundary>
51 </body>
52 </html>
53 );
54}React Server Components fundamentally change how we think about React applications, offering better performance, automatic code splitting, and safer server-side operations.