React Server Components (RSC) represent the biggest shift in React architecture since hooks. If you've been confused by "use client", mystified by server actions, or uncertain when to use each, this guide will clear things up.
React Server Components are React components that execute only on the server. They're rendered to a special format and sent to the browser, where they're hydrated without the component JavaScript ever being downloaded.
Key insight: RSC is NOT server-side rendering (SSR). SSR renders components to HTML strings. RSC renders components to a serializable tree that React can understand and hydrate.
Think of your React tree as having two types of components:
Server Components (default in Next.js 13+):
Client Components (marked with 'use client'):
Consider a markdown blog post component:
// Before: Ships 50KB of markdown libraries to client import { marked } from 'marked'; import DOMPurify from 'dompurify'; export function BlogPost({ content }) { return <div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(marked(content)) }} />; }
// After: Server Component, zero client-side markdown code import { marked } from 'marked'; import sanitize from 'sanitize-html'; export async function BlogPost({ slug }) { const content = await db.posts.findUnique({ where: { slug } }); const html = sanitize(marked(content.body)); return <article dangerouslySetInnerHTML={{ __html: html }} />; }
The markdown parsing happens server-side. The client receives only the rendered HTML.
Server Components can access your database directly—no API routes needed:
// Server Component import { db } from '@/lib/db'; export async function UserList() { const users = await db.user.findMany({ take: 10, orderBy: { createdAt: 'desc' } }); return ( <ul> {users.map(user => ( <li key={user.id}>{user.name}</li> ))} </ul> ); }
No useEffect, no loading states for initial data, no API endpoints to maintain.
// page.tsx (Server Component) import { db } from '@/lib/db'; import { LikeButton } from './LikeButton'; export default async function PostPage({ params }) { const post = await db.post.findUnique({ where: { id: params.id } }); return ( <article> <h1>{post.title}</h1> <p>{post.content}</p> <LikeButton postId={post.id} initialLikes={post.likes} /> </article> ); }
// LikeButton.tsx (Client Component) 'use client'; import { useState } from 'react'; export function LikeButton({ postId, initialLikes }) { const [likes, setLikes] = useState(initialLikes); const handleLike = async () => { setLikes(l => l + 1); await fetch(`/api/posts/${postId}/like`, { method: 'POST' }); }; return <button onClick={handleLike}>❤️ {likes}</button>; }
Server Actions let you mutate data without API routes:
// actions.ts 'use server'; import { db } from '@/lib/db'; import { revalidatePath } from 'next/cache'; export async function createPost(formData: FormData) { const title = formData.get('title'); const content = formData.get('content'); await db.post.create({ data: { title, content } }); revalidatePath('/posts'); }
// CreatePostForm.tsx 'use client'; import { createPost } from './actions'; export function CreatePostForm() { return ( <form action={createPost}> <input name="title" placeholder="Title" /> <textarea name="content" placeholder="Content" /> <button type="submit">Create Post</button> </form> ); }
Server Components enable streaming UI:
// page.tsx import { Suspense } from 'react'; export default function Dashboard() { return ( <div> <h1>Dashboard</h1> <Suspense fallback={<StatsSkeleton />}> <Stats /> </Suspense> <Suspense fallback={<ChartSkeleton />}> <Chart /> </Suspense> </div> ); } async function Stats() { const stats = await fetchSlowStats(); // Takes 2 seconds return <StatsDisplay data={stats} />; }
The page streams incrementally—fast parts render immediately while slow parts show loading states.
Don't add "use client" to every component out of habit. Start server-first and only add it when you need interactivity.
Client Components can't be imported asynchronously in Server Components:
// ❌ Wrong const DynamicClient = dynamic(() => import('./ClientComponent')); // ✅ Correct - import directly import { ClientComponent } from './ClientComponent';
Props passed from Server to Client Components must be serializable:
// ❌ Wrong - functions aren't serializable <ClientComponent onClick={() => doSomething()} /> // ✅ Correct - use server actions or pass only data <ClientComponent onClickAction={serverAction} />
| Use Server Components | Use Client Components |
|---|---|
| Fetching data | Event handlers (onClick, onChange) |
| Accessing backend resources | Browser APIs (localStorage, etc.) |
| Keeping sensitive info server-side | useState, useEffect, custom hooks |
| Reducing bundle size | Third-party client-only libs |
React Server Components aren't optional knowledge anymore—they're the default in Next.js and increasingly common elsewhere. The mental shift is significant: think about which code needs to run where, rather than treating everything as client JavaScript.
Start simple: make everything a Server Component by default, and add "use client" only when you hit a wall. You'll be surprised how much can stay server-side.