Next.js 13 introduced React Server Components (RSC), which is a major architectural change that completely changes the way we build applications in Next.js.
What are React Server Components?
React Server Components are a new type of component that render on the server instead of the client. This means:
- Server-side rendering: Components execute on the server and generate HTML
- Zero client-side JavaScript: Server components don't send any JavaScript to the client
- Direct access to backend resources: Can directly access databases, file systems, etc.
- Keep code private: Server code is not exposed to the client
Server Components vs Client Components
Server Components (Default)
javascript// By default, all components are server components async function BlogList() { // Can directly access database const posts = await db.post.findMany(); // Can use file system const content = await fs.readFile('./content.md', 'utf-8'); return ( <div> {posts.map(post => ( <PostCard key={post.id} post={post} /> ))} </div> ); }
Features:
- Render on server
- Cannot use React Hooks (useState, useEffect, etc.)
- Cannot use browser APIs (window, document, etc.)
- Cannot use event handlers (onClick, onChange, etc.)
- Can directly access databases and file systems
- Don't send JavaScript to client
Client Components
javascript'use client'; import { useState, useEffect } from 'react'; export default function InteractiveCounter() { const [count, setCount] = useState(0); useEffect(() => { // Can use browser APIs document.title = `Count: ${count}`; }, [count]); return ( <div> <p>Count: {count}</p> <button onClick={() => setCount(c => c + 1)}>Increment</button> </div> ); }
Features:
- Render on client
- Can use all React Hooks
- Can use browser APIs
- Can use event handlers
- Cannot directly access databases
- Send JavaScript to client
Mixing Server and Client Components
Using Client Components in Server Components
javascript// Server component async function BlogPage() { const posts = await fetchPosts(); return ( <div> <h1>Blog Posts</h1> <PostList posts={posts} /> <LikeButton postId={posts[0].id} /> </div> ); } // Client component 'use client'; function LikeButton({ postId }) { const [liked, setLiked] = useState(false); return ( <button onClick={() => setLiked(!liked)}> {liked ? '❤️' : '🤍'} </button> ); }
Using Server Components in Client Components
javascript// Client component 'use client'; import dynamic from 'next/dynamic'; // Dynamically import server component const ServerComponent = dynamic(() => import('./ServerComponent'), { ssr: true }); export default function ClientComponent() { return ( <div> <h1>Client Component</h1> <ServerComponent /> </div> ); }
Advantages of Server Components
1. Reduce Client-side JavaScript
javascript// Traditional approach (client component) 'use client'; import { useState, useEffect } from 'react'; function BlogList() { const [posts, setPosts] = useState([]); useEffect(() => { fetch('/api/posts') .then(res => res.json()) .then(setPosts); }, []); return ( <div> {posts.map(post => <Post key={post.id} {...post} />)} </div> ); } // Server component approach async function BlogList() { const posts = await db.post.findMany(); return ( <div> {posts.map(post => <Post key={post.id} {...post} />)} </div> ); }
2. Direct Database Access
javascriptimport { prisma } from '@/lib/prisma'; async function UserDashboard({ userId }) { // Direct database access, no API route needed const user = await prisma.user.findUnique({ where: { id: userId }, include: { posts: true, comments: true } }); return ( <div> <h1>Welcome, {user.name}</h1> <p>You have {user.posts.length} posts</p> </div> ); }
3. Keep Code Private
javascript// Sensitive code in server components won't be exposed to client async function AdminPanel() { const apiKey = process.env.SECRET_API_KEY; // This API call won't be exposed to client const data = await fetch(`https://api.example.com?key=${apiKey}`) .then(res => res.json()); return <div>{data.content}</div>; }
4. Better Performance
javascript// Server components can fetch data in parallel async function Dashboard() { const [user, posts, notifications] = await Promise.all([ fetchUser(), fetchPosts(), fetchNotifications() ]); return ( <div> <UserProfile user={user} /> <PostList posts={posts} /> <NotificationList notifications={notifications} /> </div> ); }
Real-world Use Cases
1. Blog Post List
javascript// app/blog/page.js async function BlogPage() { const posts = await db.post.findMany({ orderBy: { createdAt: 'desc' }, take: 10 }); return ( <div> <h1>Latest Posts</h1> {posts.map(post => ( <PostCard key={post.id} post={post} /> ))} </div> ); } // components/PostCard.js export default function PostCard({ post }) { return ( <article> <h2>{post.title}</h2> <p>{post.excerpt}</p> <Link href={`/blog/${post.slug}`}>Read more</Link> </article> ); }
2. E-commerce Product Page
javascript// app/products/[id]/page.js async function ProductPage({ params }) { const product = await db.product.findUnique({ where: { id: params.id }, include: { reviews: true, relatedProducts: true } }); return ( <div> <ProductDetails product={product} /> <ProductReviews reviews={product.reviews} /> <RelatedProducts products={product.relatedProducts} /> <AddToCartButton productId={product.id} /> </div> ); } 'use client'; function AddToCartButton({ productId }) { const [loading, setLoading] = useState(false); const handleAddToCart = async () => { setLoading(true); await fetch('/api/cart', { method: 'POST', body: JSON.stringify({ productId }) }); setLoading(false); }; return ( <button onClick={handleAddToCart} disabled={loading}> {loading ? 'Adding...' : 'Add to Cart'} </button> ); }
3. Dashboard
javascript// app/dashboard/page.js import { auth } from '@/auth'; async function Dashboard() { const session = await auth(); const [stats, recentActivity, notifications] = await Promise.all([ getUserStats(session.user.id), getRecentActivity(session.user.id), getNotifications(session.user.id) ]); return ( <div> <DashboardStats stats={stats} /> <RecentActivity activities={recentActivity} /> <NotificationPanel notifications={notifications} /> </div> ); }
Best Practices
1. Use Server Components by Default
javascript// ✅ Good practice async function Page() { const data = await fetchData(); return <div>{data.content}</div>; } // ❌ Bad practice 'use client'; function Page() { const [data, setData] = useState(null); useEffect(() => { fetchData().then(setData); }, []); return <div>{data?.content}</div>; }
2. Only Use Client Components Where Interaction is Needed
javascript// Server component async function ProductList() { const products = await fetchProducts(); return ( <div> {products.map(product => ( <ProductCard key={product.id} product={product} /> ))} </div> ); } // Only use 'use client' in child components that need interaction 'use client'; function ProductCard({ product }) { const [liked, setLiked] = useState(false); return ( <div> <h3>{product.name}</h3> <button onClick={() => setLiked(!liked)}> {liked ? '❤️' : '🤍'} </button> </div> ); }
3. Move Client Components to Bottom of Component Tree
javascript// ✅ Good practice: client components at bottom async function Page() { const data = await fetchData(); return ( <div> <Header /> <Content data={data} /> <InteractiveWidget /> </div> ); } 'use client'; function InteractiveWidget() { // Interaction logic } // ❌ Bad practice: client components at top 'use client'; function Page() { const [data, setData] = useState(null); useEffect(() => { fetchData().then(setData); }, []); return ( <div> <Header /> <Content data={data} /> </div> ); }
4. Use Dynamic Imports to Reduce Client JavaScript
javascriptimport dynamic from 'next/dynamic'; // Dynamically import heavy components const HeavyComponent = dynamic(() => import('./HeavyComponent'), { loading: () => <div>Loading...</div>, ssr: false // Disable server-side rendering }); async function Page() { const data = await fetchData(); return ( <div> <LightContent data={data} /> <HeavyComponent /> </div> ); }
Common Questions
Q: How to use state in server components?
A: Server components can't use useState, but you can handle it this way:
javascript// Use URL parameters to manage state async function Page({ searchParams }) { const page = parseInt(searchParams.page || '1'); const posts = await getPosts(page); return ( <div> <PostList posts={posts} /> <Pagination currentPage={page} /> </div> ); } 'use client'; function Pagination({ currentPage }) { const router = useRouter(); return ( <div> <button onClick={() => router.push(`?page=${currentPage - 1}`)}> Previous </button> <button onClick={() => router.push(`?page=${currentPage + 1}`)}> Next </button> </div> ); }
Q: How to handle form submission in server components?
A: Use Server Actions:
javascript'use server'; import { revalidatePath } from 'next/cache'; export async function createPost(formData) { const title = formData.get('title'); const content = formData.get('content'); await db.post.create({ data: { title, content } }); revalidatePath('/blog'); } // Use in component import { createPost } from './actions'; export default function CreatePostForm() { return ( <form action={createPost}> <input name="title" /> <textarea name="content" /> <button type="submit">Create</button> </form> ); }
React Server Components are the future of Next.js. By properly using server components and client components, you can build applications with better performance and user experience.