Server Components render on the server with zero client JavaScript. Here's how to use them effectively.
Server vs Client Components#
1// Server Component (default in App Router)
2// Can access database, file system, environment variables
3async function UserProfile({ userId }: { userId: string }) {
4 const user = await db.users.findUnique({ where: { id: userId } });
5
6 return (
7 <div>
8 <h1>{user.name}</h1>
9 <p>{user.bio}</p>
10 </div>
11 );
12}
13
14// Client Component - add 'use client' directive
15'use client';
16
17import { useState } from 'react';
18
19function Counter() {
20 const [count, setCount] = useState(0);
21
22 return (
23 <button onClick={() => setCount(c => c + 1)}>
24 Count: {count}
25 </button>
26 );
27}When to Use Each#
| Server Components | Client Components |
|---|---|
| Fetch data | Event handlers (onClick, onChange) |
| Access backend resources | useState, useEffect |
| Keep sensitive info on server | Browser APIs |
| Large dependencies | Interactivity |
Data Fetching Patterns#
1// Parallel data fetching
2async function Dashboard() {
3 // These fetch in parallel
4 const [user, posts, analytics] = await Promise.all([
5 getUser(),
6 getPosts(),
7 getAnalytics(),
8 ]);
9
10 return (
11 <div>
12 <UserCard user={user} />
13 <PostList posts={posts} />
14 <AnalyticsChart data={analytics} />
15 </div>
16 );
17}
18
19// Sequential when needed
20async function PostPage({ postId }: { postId: string }) {
21 const post = await getPost(postId);
22 const comments = await getComments(post.id); // Needs post.id
23
24 return <Post post={post} comments={comments} />;
25}Mixing Server and Client#
1// Server Component
2async function ProductPage({ id }: { id: string }) {
3 const product = await getProduct(id);
4
5 return (
6 <div>
7 <h1>{product.name}</h1>
8 <p>{product.description}</p>
9 {/* Client Component for interactivity */}
10 <AddToCartButton productId={id} price={product.price} />
11 </div>
12 );
13}
14
15// Client Component
16'use client';
17
18function AddToCartButton({ productId, price }: Props) {
19 const [adding, setAdding] = useState(false);
20
21 const handleAdd = async () => {
22 setAdding(true);
23 await addToCart(productId);
24 setAdding(false);
25 };
26
27 return (
28 <button onClick={handleAdd} disabled={adding}>
29 Add to Cart - ${price}
30 </button>
31 );
32}Passing Server Data to Client#
1// ✅ Pass serializable props
2async function Page() {
3 const data = await fetchData();
4 return <ClientComponent data={data} />;
5}
6
7// ❌ Can't pass functions or classes
8async function Page() {
9 const handler = () => console.log('click');
10 return <ClientComponent onClick={handler} />; // Error!
11}
12
13// ✅ Use Server Actions for mutations
14async function Page() {
15 async function submitForm(formData: FormData) {
16 'use server';
17 await saveToDatabase(formData);
18 }
19
20 return <ClientForm action={submitForm} />;
21}Streaming with Suspense#
1import { Suspense } from 'react';
2
3async function Page() {
4 return (
5 <div>
6 <Header /> {/* Renders immediately */}
7
8 <Suspense fallback={<ProductSkeleton />}>
9 <ProductList /> {/* Streams when ready */}
10 </Suspense>
11
12 <Suspense fallback={<ReviewsSkeleton />}>
13 <Reviews /> {/* Streams independently */}
14 </Suspense>
15 </div>
16 );
17}Best Practices#
- Start with Server Components - Only add 'use client' when needed
- Push client boundaries down - Keep interactive parts small
- Fetch data at the top - Pass down as props
- Use Suspense for loading states - Better UX than full page loading
Server Components reduce bundle size, improve performance, and simplify data fetching.