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.tsx3import { db } from '@/lib/db';4import { cache } from 'react';5import AdSpace from '@/components/AdSpace';67// Server Components can be async and fetch data directly8export default async function ProductsPage() {9 // Direct database access - no API route needed10 const products = await db.product.findMany({11 where: { published: true },12 orderBy: { createdAt: 'desc' },13 include: { category: true }14 });1516 // This runs only on the server17 console.log('Fetching products on server');1819 return (20 <div className="products-grid">21 <h1>Our Products</h1>2223 {/* Server Components can render other Server Components */}24 <ProductFilters categories={await getCategories()} />2526 <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} />3132 {/* 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}4243// Cached function for data fetching44const getCategories = cache(async () => {45 return db.category.findMany({46 orderBy: { name: 'asc' }47 });48});4950// Another Server Component51async function ProductCard({ product }: { product: Product }) {52 // Can perform async operations53 const reviews = await db.review.findMany({54 where: { productId: product.id },55 take: 556 });5758 const averageRating = reviews.reduce(59 (sum, review) => sum + review.rating, 060 ) / reviews.length || 0;6162 return (63 <article className="product-card">64 <img65 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>7273 {/* Static rendering - no JavaScript needed */}74 <div className="rating">75 {Array.from({ length: 5 }, (_, i) => (76 <span77 key={i}78 className={i < Math.round(averageRating) ? 'star filled' : 'star'}79 >80 ★81 </span>82 ))}83 <span>({reviews.length} reviews)</span>84 </div>8586 {/* 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 mutations2// app/actions/products.ts3'use server'; // Mark file as Server Actions45import { db } from '@/lib/db';6import { revalidatePath, revalidateTag } from 'next/cache';7import { redirect } from 'next/navigation';8import { z } from 'zod';9import { auth } from '@/lib/auth';1011// Schema validation12const 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});1920// Server Action for creating a product21export async function createProduct(22 prevState: any,23 formData: FormData24) {25 // Authentication check26 const session = await auth();27 if (!session?.user?.isAdmin) {28 return {29 error: 'Unauthorized. Admin access required.'30 };31 }3233 // Parse and validate form data34 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 });4142 if (!validatedFields.success) {43 return {44 error: 'Invalid form data',45 errors: validatedFields.error.flatten().fieldErrors46 };47 }4849 try {50 // Create product in database51 const product = await db.product.create({52 data: {53 ...validatedFields.data,54 userId: session.user.id55 }56 });5758 // Revalidate cached data59 revalidatePath('/products');60 revalidateTag('products');6162 // Redirect to new product page63 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}7172// Server Action for updating a product73export 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 }8182 const product = await db.product.update({83 where: { id: productId },84 data: updates85 });8687 // Revalidate specific paths88 revalidatePath(`/products/${productId}`);89 revalidatePath('/products');9091 return product;92}9394// Server Action for deleting a product95export async function deleteProduct(productId: string) {96 const session = await auth();97 if (!session?.user?.isAdmin) {98 return { error: 'Unauthorized' };99 }100101 try {102 await db.product.delete({103 where: { id: productId }104 });105106 revalidatePath('/products');107 return { success: true };108 } catch (error) {109 return { error: 'Failed to delete product' };110 }111}112113// Server Action with optimistic updates114export async function toggleFavorite(productId: string) {115 const session = await auth();116 if (!session?.user) {117 throw new Error('Must be logged in');118 }119120 const existing = await db.favorite.findUnique({121 where: {122 userId_productId: {123 userId: session.user.id,124 productId125 }126 }127 });128129 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 productId138 }139 });140 }141142 // Revalidate user's favorites143 revalidatePath('/favorites');144145 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 boundaries2// app/dashboard/page.tsx3import { Suspense } from 'react';4import AdSpace from '@/components/AdSpace';56// Loading components7function 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}1617function ChartsLoading() {18 return <div className="shimmer h-64 rounded-lg" />;19}2021// Main dashboard page22export default function DashboardPage() {23 return (24 <div className="dashboard">25 <h1>Analytics Dashboard</h1>2627 {/* Fast static content renders immediately */}28 <div className="quick-actions">29 <button>Export Data</button>30 <button>Share Report</button>31 </div>3233 {/* Stats section with streaming */}34 <Suspense fallback={<StatsLoading />}>35 <DashboardStats />36 </Suspense>3738 <AdSpace slot="dashboard-top" />3940 {/* Multiple Suspense boundaries for granular loading */}41 <div className="grid grid-cols-2 gap-6">42 <Suspense fallback={<ChartsLoading />}>43 <RevenueChart />44 </Suspense>4546 <Suspense fallback={<ChartsLoading />}>47 <TrafficChart />48 </Suspense>49 </div>5051 {/* Nested Suspense for complex components */}52 <Suspense fallback={<div>Loading activity...</div>}>53 <RecentActivity />54 </Suspense>5556 <AdSpace slot="dashboard-bottom" />57 </div>58 );59}6061// Async Server Component - will stream when ready62async function DashboardStats() {63 // Simulate slow data fetch64 const stats = await fetch('https://api.example.com/stats', {65 // Opt into dynamic rendering66 cache: 'no-store'67 }).then(r => r.json());6869 await new Promise(resolve => setTimeout(resolve, 1000));7071 return (72 <div className="stats-grid">73 <StatCard74 title="Total Revenue"75 value={`$${stats.revenue.toLocaleString()}`}76 change={stats.revenueChange}77 />78 <StatCard79 title="Active Users"80 value={stats.users.toLocaleString()}81 change={stats.userChange}82 />83 <StatCard84 title="Conversion Rate"85 value={`${stats.conversionRate}%`}86 change={stats.conversionChange}87 />88 </div>89 );90}9192// Component with error boundary93async function RevenueChart() {94 try {95 const data = await fetchRevenueData();9697 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 server105 return (106 <div className="error-state">107 <p>Failed to load revenue data</p>108 <RefreshButton />109 </div>110 );111 }112}113114// Parallel data fetching115async function RecentActivity() {116 // Fetch multiple data sources in parallel117 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 ]);131132 return (133 <div className="recent-activity">134 <h2>Recent Activity</h2>135136 <div className="activity-feed">137 {activities.map(activity => (138 <ActivityItem key={activity.id} activity={activity} />139 ))}140 </div>141142 <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 Components2// app/blog/[slug]/page.tsx3import { notFound } from 'next/navigation';4import { cache } from 'react';5import { unstable_cache } from 'next/cache';67// 1. Request Memoization - dedupes requests in single render8const 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 });1718 if (!post) notFound();19 return post;20});2122// 2. Data Cache - persists across requests23const getCachedPost = unstable_cache(24 async (slug: string) => getPost(slug),25 ['post-by-slug'], // Cache key26 {27 revalidate: 3600, // Revalidate after 1 hour28 tags: ['posts'] // Cache tags for invalidation29 }30);3132// 3. Full Route Cache - static generation33export default async function BlogPost({34 params35}: {36 params: { slug: string }37}) {38 const post = await getCachedPost(params.slug);3940 // Track views asynchronously (doesn't block render)41 trackPageView(post.id);4243 return (44 <article>45 <header>46 <h1>{post.title}</h1>47 <BlogMeta post={post} />48 </header>4950 <AdSpace slot="blog-top" />5152 <div className="prose">53 {post.content}54 </div>5556 {/* Dynamically import heavy components */}57 <Suspense fallback={<CommentsSkeleton />}>58 <Comments postId={post.id} />59 </Suspense>6061 <AdSpace slot="blog-bottom" />6263 <RelatedPosts categoryId={post.categoryId} />64 </article>65 );66}6768// 4. Partial Prerendering (experimental)69export const experimental_ppr = true;7071// Related posts with different cache strategy72async function RelatedPosts({ categoryId }: { categoryId: string }) {73 // Different cache duration for less critical data74 const posts = await unstable_cache(75 async () => {76 return db.post.findMany({77 where: {78 categoryId,79 published: true80 },81 orderBy: { views: 'desc' },82 take: 5,83 select: {84 id: true,85 title: true,86 slug: true,87 excerpt: true,88 imageUrl: true89 }90 });91 },92 ['related-posts', categoryId],93 {94 revalidate: 7200, // 2 hours95 tags: ['posts', `category-${categoryId}`]96 }97 )();9899 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}110111// 5. On-demand revalidation112// app/api/revalidate/route.ts113import { NextRequest } from 'next/server';114import { revalidatePath, revalidateTag } from 'next/cache';115116export async function POST(request: NextRequest) {117 const { secret, path, tag } = await request.json();118119 // Verify webhook secret120 if (secret !== process.env.REVALIDATION_SECRET) {121 return new Response('Invalid secret', { status: 401 });122 }123124 try {125 if (tag) {126 // Revalidate by cache tag127 revalidateTag(tag);128 } else if (path) {129 // Revalidate specific path130 revalidatePath(path);131 }132133 return Response.json({ revalidated: true });134 } catch (error) {135 return new Response('Error revalidating', { status: 500 });136 }137}138139// 6. Dynamic rendering opt-in140export async function CommentSection({ postId }: { postId: string }) {141 // Force dynamic rendering for real-time data142 const { cookies } = await import('next/headers');143 const userToken = cookies().get('session')?.value;144145 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 });155156 return (157 <section className="comments">158 <h2>Comments ({comments.length})</h2>159160 {userToken && <CommentForm postId={postId} />}161162 <div className="comment-list">163 {comments.map(comment => (164 <Comment165 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 patterns2// 1. Component Composition Pattern3// app/components/DataBoundary.tsx4import { Suspense } from 'react';5import { ErrorBoundary } from 'react-error-boundary';67interface DataBoundaryProps<T> {8 fallback?: React.ReactNode;9 errorFallback?: React.ComponentType<{ error: Error }>;10 getData: () => Promise<T>;11 children: (data: T) => React.ReactNode;12}1314export async function DataBoundary<T>({15 fallback = <div>Loading...</div>,16 errorFallback = DefaultError,17 getData,18 children19}: 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}2728// Usage29export default async function ProductsPage() {30 return (31 <DataBoundary32 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}4546// 2. Parallel Data Loading Pattern47// app/dashboard/page.tsx48export default async function Dashboard() {49 // Initiate all data fetches in parallel50 const statsPromise = getStats();51 const chartsPromise = getChartData();52 const activityPromise = getRecentActivity();53 const notificationsPromise = getNotifications();5455 return (56 <div className="dashboard">57 {/* Each component handles its own promise */}58 <Suspense fallback={<StatsSkeleton />}>59 <StatsSection dataPromise={statsPromise} />60 </Suspense>6162 <div className="grid grid-cols-2 gap-6">63 <Suspense fallback={<ChartSkeleton />}>64 <ChartSection dataPromise={chartsPromise} />65 </Suspense>6667 <Suspense fallback={<ActivitySkeleton />}>68 <ActivityFeed dataPromise={activityPromise} />69 </Suspense>70 </div>7172 <Suspense fallback={null}>73 <NotificationBar dataPromise={notificationsPromise} />74 </Suspense>75 </div>76 );77}7879// 3. Conditional Rendering Pattern80// app/components/FeatureFlag.tsx81import { headers } from 'next/headers';82import { getFeatureFlags } from '@/lib/features';8384interface FeatureFlagProps {85 feature: string;86 fallback?: React.ReactNode;87 children: React.ReactNode;88}8990export async function FeatureFlag({91 feature,92 fallback = null,93 children94}: FeatureFlagProps) {95 const headersList = headers();96 const userId = headersList.get('x-user-id');9798 const flags = await getFeatureFlags(userId);99100 if (!flags[feature]) {101 return <>{fallback}</>;102 }103104 return <>{children}</>;105}106107// Usage108export default async function HomePage() {109 return (110 <div>111 <h1>Welcome</h1>112113 <FeatureFlag feature="new-dashboard">114 <NewDashboard />115 </FeatureFlag>116117 <FeatureFlag118 feature="beta-analytics"119 fallback={<LegacyAnalytics />}120 >121 <BetaAnalytics />122 </FeatureFlag>123 </div>124 );125}126127// 4. Data Aggregation Pattern128// app/api/data/aggregate.ts129import { unstable_cache } from 'next/cache';130131// Aggregate data from multiple sources132export const getAggregatedDashboardData = unstable_cache(133 async (userId: string) => {134 // Fetch from multiple sources in parallel135 const [136 userData,137 userStats,138 userActivity,139 systemNotifications140 ] = await Promise.all([141 db.user.findUnique({ where: { id: userId } }),142 getStatsFromAnalytics(userId),143 getActivityFromEventStore(userId),144 getSystemNotifications()145 ]);146147 // Transform and combine data148 return {149 user: {150 ...userData,151 stats: userStats152 },153 activity: processActivityData(userActivity),154 notifications: filterRelevantNotifications(155 systemNotifications,156 userData157 ),158 summary: generateDashboardSummary({159 userData,160 userStats,161 userActivity162 })163 };164 },165 ['dashboard-data'],166 {167 revalidate: 300, // 5 minutes168 tags: ['dashboard', 'user-data']169 }170);171172// 5. Secure Data Access Pattern173// app/lib/data-access.ts174import { auth } from '@/lib/auth';175import { forbidden, unauthorized } from 'next/navigation';176177// Secure data fetching wrapper178export 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();186187 if (!session?.user) {188 unauthorized();189 }190191 // Check role192 if (options?.requiredRole && session.user.role !== options.requiredRole) {193 forbidden();194 }195196 // Check permissions197 if (options?.requiredPermissions) {198 const hasPermissions = options.requiredPermissions.every(199 permission => session.user.permissions.includes(permission)200 );201202 if (!hasPermissions) {203 forbidden();204 }205 }206207 // Execute fetcher with user context208 return fetcher(session.user.id);209}210211// Usage212export 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 );224225 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.