React Server Components (RSC) render on the server, reducing client JavaScript while enabling direct database access. Here's how they work.
Server vs Client Components#
1// Server Component (default in App Router)
2// app/page.js
3async function HomePage() {
4 // Can use async/await directly
5 const data = await fetchFromDatabase();
6
7 return (
8 <main>
9 <h1>Welcome</h1>
10 <DataDisplay data={data} />
11 </main>
12 );
13}
14
15// Client Component (needs 'use client' directive)
16// components/Counter.js
17'use client';
18
19import { useState } from 'react';
20
21function Counter() {
22 const [count, setCount] = useState(0);
23
24 return (
25 <button onClick={() => setCount(count + 1)}>
26 Count: {count}
27 </button>
28 );
29}Data Fetching#
1// Server Component with data fetching
2// app/users/page.js
3async function UsersPage() {
4 // Fetch directly - no useEffect needed
5 const users = await prisma.user.findMany();
6
7 return (
8 <div>
9 <h1>Users</h1>
10 <ul>
11 {users.map(user => (
12 <li key={user.id}>{user.name}</li>
13 ))}
14 </ul>
15 </div>
16 );
17}
18
19// With error handling
20async function SafeDataFetch() {
21 try {
22 const data = await fetchData();
23 return <DataView data={data} />;
24 } catch (error) {
25 return <ErrorMessage error={error} />;
26 }
27}Mixing Server and Client#
1// Server Component
2// app/dashboard/page.js
3import { ClientChart } from './ClientChart';
4
5async function Dashboard() {
6 // Fetch on server
7 const stats = await getStats();
8
9 return (
10 <div>
11 <h1>Dashboard</h1>
12
13 {/* Pass data to client component */}
14 <ClientChart data={stats.chartData} />
15
16 {/* Server-rendered static content */}
17 <StatsSummary stats={stats} />
18 </div>
19 );
20}
21
22// Client Component
23// app/dashboard/ClientChart.js
24'use client';
25
26import { useEffect, useRef } from 'react';
27import Chart from 'chart.js';
28
29export function ClientChart({ data }) {
30 const canvasRef = useRef(null);
31
32 useEffect(() => {
33 const chart = new Chart(canvasRef.current, {
34 type: 'line',
35 data: data,
36 });
37
38 return () => chart.destroy();
39 }, [data]);
40
41 return <canvas ref={canvasRef} />;
42}Composition Patterns#
1// Server Component wrapping Client Component
2// app/posts/page.js
3import { InteractiveList } from './InteractiveList';
4
5async function PostsPage() {
6 const posts = await getPosts();
7
8 return (
9 <div>
10 <h1>Posts</h1>
11
12 {/* Server data passed to client interactivity */}
13 <InteractiveList
14 items={posts}
15 renderItem={(post) => (
16 <PostCard key={post.id} post={post} />
17 )}
18 />
19 </div>
20 );
21}
22
23// Client Component for interactivity
24'use client';
25
26export function InteractiveList({ items, renderItem }) {
27 const [filter, setFilter] = useState('');
28
29 const filtered = items.filter(item =>
30 item.title.toLowerCase().includes(filter.toLowerCase())
31 );
32
33 return (
34 <div>
35 <input
36 value={filter}
37 onChange={e => setFilter(e.target.value)}
38 placeholder="Filter..."
39 />
40 <ul>
41 {filtered.map(renderItem)}
42 </ul>
43 </div>
44 );
45}Streaming with Suspense#
1// app/page.js
2import { Suspense } from 'react';
3
4async function SlowComponent() {
5 const data = await slowFetch(); // Takes 3 seconds
6 return <div>{data}</div>;
7}
8
9export default function Page() {
10 return (
11 <div>
12 <h1>Dashboard</h1>
13
14 {/* Fast content renders immediately */}
15 <QuickStats />
16
17 {/* Slow content streams in */}
18 <Suspense fallback={<Loading />}>
19 <SlowComponent />
20 </Suspense>
21
22 {/* Multiple suspense boundaries */}
23 <Suspense fallback={<ChartSkeleton />}>
24 <AsyncChart />
25 </Suspense>
26 </div>
27 );
28}Server Actions#
1// Server Action in Server Component
2// app/posts/page.js
3async function PostsPage() {
4 async function createPost(formData) {
5 'use server';
6
7 const title = formData.get('title');
8 const content = formData.get('content');
9
10 await prisma.post.create({
11 data: { title, content },
12 });
13
14 revalidatePath('/posts');
15 }
16
17 return (
18 <form action={createPost}>
19 <input name="title" placeholder="Title" />
20 <textarea name="content" placeholder="Content" />
21 <button type="submit">Create Post</button>
22 </form>
23 );
24}
25
26// Server Action with Client Component
27// actions.js
28'use server';
29
30export async function submitForm(formData) {
31 const data = Object.fromEntries(formData);
32 await saveToDatabase(data);
33 revalidatePath('/');
34}
35
36// components/Form.js
37'use client';
38
39import { submitForm } from '../actions';
40import { useFormStatus } from 'react-dom';
41
42function SubmitButton() {
43 const { pending } = useFormStatus();
44 return (
45 <button disabled={pending}>
46 {pending ? 'Submitting...' : 'Submit'}
47 </button>
48 );
49}
50
51export function ContactForm() {
52 return (
53 <form action={submitForm}>
54 <input name="email" type="email" required />
55 <textarea name="message" required />
56 <SubmitButton />
57 </form>
58 );
59}Caching and Revalidation#
1// Cached fetch (default)
2async function CachedData() {
3 const data = await fetch('https://api.example.com/data');
4 // Cached indefinitely by default
5 return <div>{data}</div>;
6}
7
8// Revalidate after time
9async function TimedRevalidation() {
10 const data = await fetch('https://api.example.com/data', {
11 next: { revalidate: 3600 }, // Revalidate every hour
12 });
13 return <div>{data}</div>;
14}
15
16// No caching
17async function FreshData() {
18 const data = await fetch('https://api.example.com/data', {
19 cache: 'no-store',
20 });
21 return <div>{data}</div>;
22}
23
24// Manual revalidation
25import { revalidatePath, revalidateTag } from 'next/cache';
26
27async function updatePost(id, data) {
28 'use server';
29
30 await prisma.post.update({ where: { id }, data });
31
32 revalidatePath('/posts');
33 revalidateTag('posts');
34}Database Access#
1// Direct database access in Server Components
2// app/users/[id]/page.js
3import { prisma } from '@/lib/prisma';
4import { notFound } from 'next/navigation';
5
6async function UserPage({ params }) {
7 const user = await prisma.user.findUnique({
8 where: { id: params.id },
9 include: {
10 posts: true,
11 profile: true,
12 },
13 });
14
15 if (!user) {
16 notFound();
17 }
18
19 return (
20 <div>
21 <h1>{user.name}</h1>
22 <p>{user.email}</p>
23
24 <h2>Posts</h2>
25 <ul>
26 {user.posts.map(post => (
27 <li key={post.id}>{post.title}</li>
28 ))}
29 </ul>
30 </div>
31 );
32}Loading States#
1// app/posts/loading.js
2export default function Loading() {
3 return (
4 <div className="loading">
5 <div className="skeleton" />
6 <div className="skeleton" />
7 <div className="skeleton" />
8 </div>
9 );
10}
11
12// app/posts/page.js
13async function PostsPage() {
14 const posts = await getPosts(); // loading.js shows during this
15
16 return (
17 <ul>
18 {posts.map(post => (
19 <li key={post.id}>{post.title}</li>
20 ))}
21 </ul>
22 );
23}Error Handling#
1// app/posts/error.js
2'use client';
3
4export default function Error({ error, reset }) {
5 return (
6 <div className="error">
7 <h2>Something went wrong!</h2>
8 <p>{error.message}</p>
9 <button onClick={() => reset()}>Try again</button>
10 </div>
11 );
12}
13
14// app/posts/page.js
15async function PostsPage() {
16 const posts = await getPosts(); // If this throws, error.js renders
17
18 return <PostList posts={posts} />;
19}Best Practices#
Server Components:
✓ Use for static content
✓ Direct data fetching
✓ Database queries
✓ Reduce client JS
Client Components:
✓ Event handlers (onClick, etc.)
✓ Browser APIs
✓ State (useState, useReducer)
✓ Effects (useEffect)
Patterns:
✓ Fetch data at the top
✓ Pass to client components as props
✓ Use Suspense for streaming
✓ Server Actions for mutations
Avoid:
✗ 'use client' on everything
✗ Fetching in client components
✗ Large client bundles
✗ Waterfall requests
Conclusion#
React Server Components reduce client JavaScript by rendering on the server with direct database access. Use them for data fetching and static content, while reserving Client Components for interactivity. Combine with Suspense for streaming, Server Actions for mutations, and proper caching for optimal performance. The key is choosing the right component type for each use case.