React Server Components Deep Dive

Master the next generation of React with Server Components. Learn streaming, server actions, caching strategies, and advanced patterns for building performant applications.

Advertisement Space - server-components-top

Google AdSense: auto

Understanding Server Components

Learn the fundamentals of React Server Components and their benefits

💡 Key Concept: React Server Components (RSC) allow components to render on the server, reducing bundle size and improving performance. They enable direct database access, automatic code splitting, and zero client-side JavaScript for static content.

1// Server Component (default in Next.js 13+ app directory)
2// app/products/page.tsx
3import { db } from '@/lib/db';
4import { cache } from 'react';
5import AdSpace from '@/components/AdSpace';
6
7// Server Components can be async and fetch data directly
8export default async function ProductsPage() {
9 // Direct database access - no API route needed
10 const products = await db.product.findMany({
11 where: { published: true },
12 orderBy: { createdAt: 'desc' },
13 include: { category: true }
14 });
15
16 // This runs only on the server
17 console.log('Fetching products on server');
18
19 return (
20 <div className="products-grid">
21 <h1>Our Products</h1>
22
23 {/* Server Components can render other Server Components */}
24 <ProductFilters categories={await getCategories()} />
25
26 <div className="grid grid-cols-3 gap-6">
27 {products.map((product, index) => (
28 <div key={product.id}>
29 {/* Server Component rendering */}
30 <ProductCard product={product} />
31
32 {/* Ad placement every 3 products */}
33 {(index + 1) % 3 === 0 && (
34 <AdSpace slot="product-list" />
35 )}
36 </div>
37 ))}
38 </div>
39 </div>
40 );
41}
42
43// Cached function for data fetching
44const getCategories = cache(async () => {
45 return db.category.findMany({
46 orderBy: { name: 'asc' }
47 });
48});
49
50// Another Server Component
51async function ProductCard({ product }: { product: Product }) {
52 // Can perform async operations
53 const reviews = await db.review.findMany({
54 where: { productId: product.id },
55 take: 5
56 });
57
58 const averageRating = reviews.reduce(
59 (sum, review) => sum + review.rating, 0
60 ) / reviews.length || 0;
61
62 return (
63 <article className="product-card">
64 <img
65 src={product.imageUrl}
66 alt={product.name}
67 className="w-full h-48 object-cover"
68 />
69 <h2>{product.name}</h2>
70 <p className="text-gray-600">{product.category.name}</p>
71 <p className="price">${product.price}</p>
72
73 {/* Static rendering - no JavaScript needed */}
74 <div className="rating">
75 {Array.from({ length: 5 }, (_, i) => (
76 <span
77 key={i}
78 className={i < Math.round(averageRating) ? 'star filled' : 'star'}
79 >
80 ★
81 </span>
82 ))}
83 <span>({reviews.length} reviews)</span>
84 </div>
85
86 {/* Client Component for interactivity */}
87 <AddToCartButton productId={product.id} />
88 </article>
89 );
90}

Server Actions & Data Mutations

Handle forms and data mutations with Server Actions

💡 Key Concept: Server Actions are asynchronous functions that run on the server and can be called from Client or Server Components. They replace traditional API routes for data mutations, providing type safety and simplified data flow.

1// Server Actions for data mutations
2// app/actions/products.ts
3'use server'; // Mark file as Server Actions
4
5import { db } from '@/lib/db';
6import { revalidatePath, revalidateTag } from 'next/cache';
7import { redirect } from 'next/navigation';
8import { z } from 'zod';
9import { auth } from '@/lib/auth';
10
11// Schema validation
12const CreateProductSchema = z.object({
13 name: z.string().min(1).max(100),
14 description: z.string().min(10).max(1000),
15 price: z.number().positive(),
16 categoryId: z.string().uuid(),
17 imageUrl: z.string().url()
18});
19
20// Server Action for creating a product
21export async function createProduct(
22 prevState: any,
23 formData: FormData
24) {
25 // Authentication check
26 const session = await auth();
27 if (!session?.user?.isAdmin) {
28 return {
29 error: 'Unauthorized. Admin access required.'
30 };
31 }
32
33 // Parse and validate form data
34 const validatedFields = CreateProductSchema.safeParse({
35 name: formData.get('name'),
36 description: formData.get('description'),
37 price: parseFloat(formData.get('price') as string),
38 categoryId: formData.get('categoryId'),
39 imageUrl: formData.get('imageUrl')
40 });
41
42 if (!validatedFields.success) {
43 return {
44 error: 'Invalid form data',
45 errors: validatedFields.error.flatten().fieldErrors
46 };
47 }
48
49 try {
50 // Create product in database
51 const product = await db.product.create({
52 data: {
53 ...validatedFields.data,
54 userId: session.user.id
55 }
56 });
57
58 // Revalidate cached data
59 revalidatePath('/products');
60 revalidateTag('products');
61
62 // Redirect to new product page
63 redirect(`/products/${product.id}`);
64 } catch (error) {
65 console.error('Failed to create product:', error);
66 return {
67 error: 'Failed to create product. Please try again.'
68 };
69 }
70}
71
72// Server Action for updating a product
73export async function updateProduct(
74 productId: string,
75 updates: Partial<Product>
76) {
77 const session = await auth();
78 if (!session?.user?.isAdmin) {
79 throw new Error('Unauthorized');
80 }
81
82 const product = await db.product.update({
83 where: { id: productId },
84 data: updates
85 });
86
87 // Revalidate specific paths
88 revalidatePath(`/products/${productId}`);
89 revalidatePath('/products');
90
91 return product;
92}
93
94// Server Action for deleting a product
95export async function deleteProduct(productId: string) {
96 const session = await auth();
97 if (!session?.user?.isAdmin) {
98 return { error: 'Unauthorized' };
99 }
100
101 try {
102 await db.product.delete({
103 where: { id: productId }
104 });
105
106 revalidatePath('/products');
107 return { success: true };
108 } catch (error) {
109 return { error: 'Failed to delete product' };
110 }
111}
112
113// Server Action with optimistic updates
114export async function toggleFavorite(productId: string) {
115 const session = await auth();
116 if (!session?.user) {
117 throw new Error('Must be logged in');
118 }
119
120 const existing = await db.favorite.findUnique({
121 where: {
122 userId_productId: {
123 userId: session.user.id,
124 productId
125 }
126 }
127 });
128
129 if (existing) {
130 await db.favorite.delete({
131 where: { id: existing.id }
132 });
133 } else {
134 await db.favorite.create({
135 data: {
136 userId: session.user.id,
137 productId
138 }
139 });
140 }
141
142 // Revalidate user's favorites
143 revalidatePath('/favorites');
144
145 return !existing;
146}

Streaming & Suspense

Implement progressive rendering with streaming and React Suspense

💡 Key Concept: Streaming allows you to progressively render UI from the server, showing content as it becomes ready. Combined with Suspense, you can create fast-loading pages that show loading states for specific parts of your UI.

1// Streaming with Suspense boundaries
2// app/dashboard/page.tsx
3import { Suspense } from 'react';
4import AdSpace from '@/components/AdSpace';
5
6// Loading components
7function StatsLoading() {
8 return (
9 <div className="stats-skeleton">
10 <div className="shimmer h-32 rounded-lg" />
11 <div className="shimmer h-32 rounded-lg" />
12 <div className="shimmer h-32 rounded-lg" />
13 </div>
14 );
15}
16
17function ChartsLoading() {
18 return <div className="shimmer h-64 rounded-lg" />;
19}
20
21// Main dashboard page
22export default function DashboardPage() {
23 return (
24 <div className="dashboard">
25 <h1>Analytics Dashboard</h1>
26
27 {/* Fast static content renders immediately */}
28 <div className="quick-actions">
29 <button>Export Data</button>
30 <button>Share Report</button>
31 </div>
32
33 {/* Stats section with streaming */}
34 <Suspense fallback={<StatsLoading />}>
35 <DashboardStats />
36 </Suspense>
37
38 <AdSpace slot="dashboard-top" />
39
40 {/* Multiple Suspense boundaries for granular loading */}
41 <div className="grid grid-cols-2 gap-6">
42 <Suspense fallback={<ChartsLoading />}>
43 <RevenueChart />
44 </Suspense>
45
46 <Suspense fallback={<ChartsLoading />}>
47 <TrafficChart />
48 </Suspense>
49 </div>
50
51 {/* Nested Suspense for complex components */}
52 <Suspense fallback={<div>Loading activity...</div>}>
53 <RecentActivity />
54 </Suspense>
55
56 <AdSpace slot="dashboard-bottom" />
57 </div>
58 );
59}
60
61// Async Server Component - will stream when ready
62async function DashboardStats() {
63 // Simulate slow data fetch
64 const stats = await fetch('https://api.example.com/stats', {
65 // Opt into dynamic rendering
66 cache: 'no-store'
67 }).then(r => r.json());
68
69 await new Promise(resolve => setTimeout(resolve, 1000));
70
71 return (
72 <div className="stats-grid">
73 <StatCard
74 title="Total Revenue"
75 value={`$${stats.revenue.toLocaleString()}`}
76 change={stats.revenueChange}
77 />
78 <StatCard
79 title="Active Users"
80 value={stats.users.toLocaleString()}
81 change={stats.userChange}
82 />
83 <StatCard
84 title="Conversion Rate"
85 value={`${stats.conversionRate}%`}
86 change={stats.conversionChange}
87 />
88 </div>
89 );
90}
91
92// Component with error boundary
93async function RevenueChart() {
94 try {
95 const data = await fetchRevenueData();
96
97 return (
98 <div className="chart-container">
99 <h2>Revenue Overview</h2>
100 <LineChart data={data} />
101 </div>
102 );
103 } catch (error) {
104 // Error UI rendered on server
105 return (
106 <div className="error-state">
107 <p>Failed to load revenue data</p>
108 <RefreshButton />
109 </div>
110 );
111 }
112}
113
114// Parallel data fetching
115async function RecentActivity() {
116 // Fetch multiple data sources in parallel
117 const [activities, users, products] = await Promise.all([
118 db.activity.findMany({
119 take: 10,
120 orderBy: { createdAt: 'desc' }
121 }),
122 db.user.findMany({
123 take: 5,
124 orderBy: { lastActive: 'desc' }
125 }),
126 db.product.findMany({
127 take: 5,
128 orderBy: { views: 'desc' }
129 })
130 ]);
131
132 return (
133 <div className="recent-activity">
134 <h2>Recent Activity</h2>
135
136 <div className="activity-feed">
137 {activities.map(activity => (
138 <ActivityItem key={activity.id} activity={activity} />
139 ))}
140 </div>
141
142 <div className="sidebar">
143 <ActiveUsers users={users} />
144 <TrendingProducts products={products} />
145 </div>
146 </div>
147 );
148}

Advertisement Space - server-components-middle

Google AdSense: auto

Caching & Revalidation Strategies

Optimize performance with intelligent caching and revalidation

💡 Key Concept: React Server Components work with Next.js caching layers to optimize performance. Learn how to implement static generation, incremental static regeneration, and on-demand revalidation for optimal performance.

1// Caching strategies in Server Components
2// app/blog/[slug]/page.tsx
3import { notFound } from 'next/navigation';
4import { cache } from 'react';
5import { unstable_cache } from 'next/cache';
6
7// 1. Request Memoization - dedupes requests in single render
8const getPost = cache(async (slug: string) => {
9 const post = await db.post.findUnique({
10 where: { slug, published: true },
11 include: {
12 author: true,
13 category: true,
14 _count: { select: { comments: true, likes: true } }
15 }
16 });
17
18 if (!post) notFound();
19 return post;
20});
21
22// 2. Data Cache - persists across requests
23const getCachedPost = unstable_cache(
24 async (slug: string) => getPost(slug),
25 ['post-by-slug'], // Cache key
26 {
27 revalidate: 3600, // Revalidate after 1 hour
28 tags: ['posts'] // Cache tags for invalidation
29 }
30);
31
32// 3. Full Route Cache - static generation
33export default async function BlogPost({
34 params
35}: {
36 params: { slug: string }
37}) {
38 const post = await getCachedPost(params.slug);
39
40 // Track views asynchronously (doesn't block render)
41 trackPageView(post.id);
42
43 return (
44 <article>
45 <header>
46 <h1>{post.title}</h1>
47 <BlogMeta post={post} />
48 </header>
49
50 <AdSpace slot="blog-top" />
51
52 <div className="prose">
53 {post.content}
54 </div>
55
56 {/* Dynamically import heavy components */}
57 <Suspense fallback={<CommentsSkeleton />}>
58 <Comments postId={post.id} />
59 </Suspense>
60
61 <AdSpace slot="blog-bottom" />
62
63 <RelatedPosts categoryId={post.categoryId} />
64 </article>
65 );
66}
67
68// 4. Partial Prerendering (experimental)
69export const experimental_ppr = true;
70
71// Related posts with different cache strategy
72async function RelatedPosts({ categoryId }: { categoryId: string }) {
73 // Different cache duration for less critical data
74 const posts = await unstable_cache(
75 async () => {
76 return db.post.findMany({
77 where: {
78 categoryId,
79 published: true
80 },
81 orderBy: { views: 'desc' },
82 take: 5,
83 select: {
84 id: true,
85 title: true,
86 slug: true,
87 excerpt: true,
88 imageUrl: true
89 }
90 });
91 },
92 ['related-posts', categoryId],
93 {
94 revalidate: 7200, // 2 hours
95 tags: ['posts', `category-${categoryId}`]
96 }
97 )();
98
99 return (
100 <aside className="related-posts">
101 <h2>Related Articles</h2>
102 <div className="grid gap-4">
103 {posts.map(post => (
104 <RelatedPostCard key={post.id} post={post} />
105 ))}
106 </div>
107 </aside>
108 );
109}
110
111// 5. On-demand revalidation
112// app/api/revalidate/route.ts
113import { NextRequest } from 'next/server';
114import { revalidatePath, revalidateTag } from 'next/cache';
115
116export async function POST(request: NextRequest) {
117 const { secret, path, tag } = await request.json();
118
119 // Verify webhook secret
120 if (secret !== process.env.REVALIDATION_SECRET) {
121 return new Response('Invalid secret', { status: 401 });
122 }
123
124 try {
125 if (tag) {
126 // Revalidate by cache tag
127 revalidateTag(tag);
128 } else if (path) {
129 // Revalidate specific path
130 revalidatePath(path);
131 }
132
133 return Response.json({ revalidated: true });
134 } catch (error) {
135 return new Response('Error revalidating', { status: 500 });
136 }
137}
138
139// 6. Dynamic rendering opt-in
140export async function CommentSection({ postId }: { postId: string }) {
141 // Force dynamic rendering for real-time data
142 const { cookies } = await import('next/headers');
143 const userToken = cookies().get('session')?.value;
144
145 const comments = await db.comment.findMany({
146 where: { postId },
147 orderBy: { createdAt: 'desc' },
148 include: {
149 author: true,
150 replies: {
151 include: { author: true }
152 }
153 }
154 });
155
156 return (
157 <section className="comments">
158 <h2>Comments ({comments.length})</h2>
159
160 {userToken && <CommentForm postId={postId} />}
161
162 <div className="comment-list">
163 {comments.map(comment => (
164 <Comment
165 key={comment.id}
166 comment={comment}
167 isAuthenticated={!!userToken}
168 />
169 ))}
170 </div>
171 </section>
172 );
173}

Patterns & Best Practices

Advanced patterns and performance optimization techniques

💡 Key Concept: Master advanced Server Component patterns including composition strategies, data fetching patterns, security considerations, and performance optimization techniques for production applications.

1// Advanced Server Component patterns
2// 1. Component Composition Pattern
3// app/components/DataBoundary.tsx
4import { Suspense } from 'react';
5import { ErrorBoundary } from 'react-error-boundary';
6
7interface DataBoundaryProps<T> {
8 fallback?: React.ReactNode;
9 errorFallback?: React.ComponentType<{ error: Error }>;
10 getData: () => Promise<T>;
11 children: (data: T) => React.ReactNode;
12}
13
14export async function DataBoundary<T>({
15 fallback = <div>Loading...</div>,
16 errorFallback = DefaultError,
17 getData,
18 children
19}: DataBoundaryProps<T>) {
20 try {
21 const data = await getData();
22 return <>{children(data)}</>;
23 } catch (error) {
24 return <errorFallback error={error as Error} />;
25 }
26}
27
28// Usage
29export default async function ProductsPage() {
30 return (
31 <DataBoundary
32 getData={() => db.product.findMany()}
33 fallback={<ProductsSkeleton />}
34 >
35 {(products) => (
36 <div className="products-grid">
37 {products.map(product => (
38 <ProductCard key={product.id} product={product} />
39 ))}
40 </div>
41 )}
42 </DataBoundary>
43 );
44}
45
46// 2. Parallel Data Loading Pattern
47// app/dashboard/page.tsx
48export default async function Dashboard() {
49 // Initiate all data fetches in parallel
50 const statsPromise = getStats();
51 const chartsPromise = getChartData();
52 const activityPromise = getRecentActivity();
53 const notificationsPromise = getNotifications();
54
55 return (
56 <div className="dashboard">
57 {/* Each component handles its own promise */}
58 <Suspense fallback={<StatsSkeleton />}>
59 <StatsSection dataPromise={statsPromise} />
60 </Suspense>
61
62 <div className="grid grid-cols-2 gap-6">
63 <Suspense fallback={<ChartSkeleton />}>
64 <ChartSection dataPromise={chartsPromise} />
65 </Suspense>
66
67 <Suspense fallback={<ActivitySkeleton />}>
68 <ActivityFeed dataPromise={activityPromise} />
69 </Suspense>
70 </div>
71
72 <Suspense fallback={null}>
73 <NotificationBar dataPromise={notificationsPromise} />
74 </Suspense>
75 </div>
76 );
77}
78
79// 3. Conditional Rendering Pattern
80// app/components/FeatureFlag.tsx
81import { headers } from 'next/headers';
82import { getFeatureFlags } from '@/lib/features';
83
84interface FeatureFlagProps {
85 feature: string;
86 fallback?: React.ReactNode;
87 children: React.ReactNode;
88}
89
90export async function FeatureFlag({
91 feature,
92 fallback = null,
93 children
94}: FeatureFlagProps) {
95 const headersList = headers();
96 const userId = headersList.get('x-user-id');
97
98 const flags = await getFeatureFlags(userId);
99
100 if (!flags[feature]) {
101 return <>{fallback}</>;
102 }
103
104 return <>{children}</>;
105}
106
107// Usage
108export default async function HomePage() {
109 return (
110 <div>
111 <h1>Welcome</h1>
112
113 <FeatureFlag feature="new-dashboard">
114 <NewDashboard />
115 </FeatureFlag>
116
117 <FeatureFlag
118 feature="beta-analytics"
119 fallback={<LegacyAnalytics />}
120 >
121 <BetaAnalytics />
122 </FeatureFlag>
123 </div>
124 );
125}
126
127// 4. Data Aggregation Pattern
128// app/api/data/aggregate.ts
129import { unstable_cache } from 'next/cache';
130
131// Aggregate data from multiple sources
132export const getAggregatedDashboardData = unstable_cache(
133 async (userId: string) => {
134 // Fetch from multiple sources in parallel
135 const [
136 userData,
137 userStats,
138 userActivity,
139 systemNotifications
140 ] = await Promise.all([
141 db.user.findUnique({ where: { id: userId } }),
142 getStatsFromAnalytics(userId),
143 getActivityFromEventStore(userId),
144 getSystemNotifications()
145 ]);
146
147 // Transform and combine data
148 return {
149 user: {
150 ...userData,
151 stats: userStats
152 },
153 activity: processActivityData(userActivity),
154 notifications: filterRelevantNotifications(
155 systemNotifications,
156 userData
157 ),
158 summary: generateDashboardSummary({
159 userData,
160 userStats,
161 userActivity
162 })
163 };
164 },
165 ['dashboard-data'],
166 {
167 revalidate: 300, // 5 minutes
168 tags: ['dashboard', 'user-data']
169 }
170);
171
172// 5. Secure Data Access Pattern
173// app/lib/data-access.ts
174import { auth } from '@/lib/auth';
175import { forbidden, unauthorized } from 'next/navigation';
176
177// Secure data fetching wrapper
178export async function secureDataFetch<T>(
179 fetcher: (userId: string) => Promise<T>,
180 options?: {
181 requiredRole?: string;
182 requiredPermissions?: string[];
183 }
184): Promise<T> {
185 const session = await auth();
186
187 if (!session?.user) {
188 unauthorized();
189 }
190
191 // Check role
192 if (options?.requiredRole && session.user.role !== options.requiredRole) {
193 forbidden();
194 }
195
196 // Check permissions
197 if (options?.requiredPermissions) {
198 const hasPermissions = options.requiredPermissions.every(
199 permission => session.user.permissions.includes(permission)
200 );
201
202 if (!hasPermissions) {
203 forbidden();
204 }
205 }
206
207 // Execute fetcher with user context
208 return fetcher(session.user.id);
209}
210
211// Usage
212export default async function AdminDashboard() {
213 const data = await secureDataFetch(
214 async (userId) => {
215 return db.adminStats.findMany({
216 where: { createdBy: userId }
217 });
218 },
219 {
220 requiredRole: 'admin',
221 requiredPermissions: ['view_analytics', 'manage_users']
222 }
223 );
224
225 return <AdminDashboardView data={data} />;
226}

Advertisement Space - server-components-bottom

Google AdSense: auto

Ready to Build with Server Components?

You've learned about Server Components, streaming, server actions, and performance optimization. Start building faster, more efficient React applications with these powerful patterns.