React Server Components: A Practical Guide
Server Components are React's biggest paradigm shift since hooks. Here's how to use them effectively.
The Mental Model
Think of your app as two layers:
text┌─────────────────────────────┐ │ Server Components │ ← Data fetching, heavy logic │ (run once, on the server) │ ├─────────────────────────────┤ │ Client Components │ ← Interactivity, state │ (run on both, hydrate) │ └─────────────────────────────┘
When to Use What
Server Components (default):
- Database queries
- API calls
- Heavy computations
- Accessing secrets
Client Components ("use client"):
- Event handlers (onClick, onChange)
- State (useState, useReducer)
- Effects (useEffect)
- Browser APIs
Practical Patterns
Pattern 1: Data Fetching
tsx// app/posts/page.tsx (Server Component) async function PostsPage() { const posts = await db.posts.findMany(); return ( <div> {posts.map(post => ( <PostCard key={post.id} post={post} /> ))} </div> ); }
No loading states. No useEffect. Just data.
Pattern 2: Mixing Server and Client
tsx// Server Component import { LikeButton } from "./LikeButton"; async function Post({ id }) { const post = await getPost(id); return ( <article> <h1>{post.title}</h1> <p>{post.content}</p> <LikeButton postId={id} initialCount={post.likes} /> </article> ); } // Client Component "use client"; export function LikeButton({ postId, initialCount }) { const [count, setCount] = useState(initialCount); // ... interaction logic }
Pattern 3: Composition Over Props
tsx// Instead of passing data through props <ClientWrapper data={await fetchData()} /> // Pass Server Components as children <ClientWrapper> <ServerDataDisplay /> {/* This fetches its own data */} </ClientWrapper>
Common Mistakes
- Making everything a Client Component: Start server, add client only when needed
- Fetching in Client Components: Move data fetching up to Server Components
- Over-using context: Server Components can't use context; prop drill or fetch directly
Performance Wins
Real numbers from migrating a Next.js app:
- JS bundle: -34%
- Time to Interactive: -28%
- Server response: +15% (more work server-side)
The tradeoff is worth it for most apps.