React Server Components
Server Components (Default)
tsx
// app/page.tsx - Server Component by default
import { db } from "@/lib/db"
export default async function Page() {
// Data fetching in Server Component
const users = await db.user.findMany()
return (
<div>
<h1>Users</h1>
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
)
}Benefits of Server Components
- Zero bundle size - Server Components don't add JavaScript to client bundle
- Direct backend access - Query databases, read files, use secrets
- Automatic code splitting - Only Client Components add to bundle
- Streaming - Send UI progressively as data loads
- No client-side waterfalls - Fetch all data in parallel on server
Client Components
tsx
// components/counter.tsx
"use client" // Required directive
import { useState } from "react"
export function Counter() {
const [count, setCount] = useState(0)
return <button onClick={() => setCount(count + 1)}>Count: {count}</button>
}When to Use Client Components
Use 'use client' when you need:
- Interactivity - onClick, onChange, event handlers
- State - useState, useReducer
- Effects - useEffect, useLayoutEffect
- Browser APIs - localStorage, window, document
- Custom hooks - Any hook using client-only features
- Class components - Component lifecycle methods
Composition Pattern
tsx
// app/page.tsx - Server Component
import { ClientWrapper } from "./client-wrapper"
import { db } from "@/lib/db"
export default async function Page() {
const data = await db.query()
return (
<div>
{/* Server Component content */}
<h1>Server Content</h1>
{/* Pass data to Client Component */}
{/* Server Component as children */}
</div>
)
}
// components/client-wrapper.tsx
;("use client")
export function ClientWrapper({
children,
initialData,
}: {
children: React.ReactNode
initialData: Data
}) {
const [data, setData] = useState(initialData)
return (
<div>
{/* Client Component UI */}
<button onClick={() => refresh()}>Refresh</button>
{/* Server Component children */}
{children}
</div>
)
}Streaming with Suspense
tsx
// app/page.tsx
import { Suspense } from "react"
import { SlowComponent } from "./slow-component"
import { FastComponent } from "./fast-component"
export default function Page() {
return (
<div>
{/* Renders immediately */}
{/* Shows fallback while loading */}
<Suspense fallback={<div>Loading...</div>}>
</div>
)
}
// components/slow-component.tsx
async function getData() {
await new Promise((resolve) => setTimeout(resolve, 3000))
return { data: "Loaded!" }
}
export async function SlowComponent() {
const data = await getData()
return <div>{data.data}</div>
}Parallel Data Fetching
tsx
// app/dashboard/page.tsx
async function getUser() {
return fetch("https://api.example.com/user")
}
async function getPosts() {
return fetch("https://api.example.com/posts")
}
export default async function Dashboard() {
// Fetch in parallel
const [user, posts] = await Promise.all([getUser(), getPosts()])
return (
<div>
</div>
)
}Sequential Data Fetching
tsx
// app/artist/[id]/page.tsx
async function getArtist(id: string) {
return fetch(`https://api.example.com/artists/${id}`)
}
async function getAlbums(artistId: string) {
return fetch(`https://api.example.com/artists/${artistId}/albums`)
}
export default async function ArtistPage({ params }: { params: { id: string } }) {
// Sequential: albums depends on artist
const artist = await getArtist(params.id)
const albums = await getAlbums(artist.id)
return (
<div>
<h1>{artist.name}</h1>
</div>
)
}Preloading Data
tsx
// lib/data.ts
import { cache } from "react"
export const getUser = cache(async (id: string) => {
return db.user.findUnique({ where: { id } })
})
// components/user-profile.tsx
export async function UserProfile({ userId }: { userId: string }) {
const user = await getUser(userId)
return <div>{user.name}</div>
}
// app/page.tsx
import { getUser } from "@/lib/data"
import { UserProfile } from "@/components/user-profile"
export default async function Page() {
// Preload
getUser("123")
return (
<div>
{/* This will use cached result */}
</div>
)
}Server Component Patterns
Pattern: Layout with Data Fetching
tsx
// app/dashboard/layout.tsx
import { auth } from "@/lib/auth"
import { db } from "@/lib/db"
export default async function DashboardLayout({ children }: { children: React.ReactNode }) {
const session = await auth()
const user = await db.user.findUnique({ where: { id: session.userId } })
return (
<div>
<main>{children}</main>
</div>
)
}Pattern: Conditional Client Components
tsx
// app/page.tsx
import { ClientComponent } from "./client-component"
export default async function Page() {
const data = await fetchData()
// Only render Client Component when needed
if (data.requiresInteractivity) {
return <ClientComponent data={data} />
}
return <div>{data.content}</div>
}Pattern: Server Component with Client Island
tsx
// app/blog/[slug]/page.tsx
import { LikeButton } from "./like-button"
export default async function BlogPost({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug)
return (
<article>
{/* Server-rendered content */}
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
{/* Client island for interactivity */}
</article>
)
}Context in Server/Client Components
tsx
// app/providers.tsx
"use client"
import { ThemeProvider } from "next-themes"
export function Providers({ children }: { children: React.ReactNode }) {
return <ThemeProvider>{children}</ThemeProvider>
}
// app/layout.tsx
import { Providers } from "./providers"
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
<Providers>{children}</Providers>
</body>
</html>
)
}Third-Party Components
tsx
// components/carousel-wrapper.tsx
"use client"
import { Carousel } from "third-party-carousel"
export function CarouselWrapper({ items }: { items: Item[] }) {
return <Carousel items={items} />
}
// app/page.tsx
import { CarouselWrapper } from "@/components/carousel-wrapper"
export default async function Page() {
const items = await fetchItems()
return <CarouselWrapper items={items} />
}Edge Runtime
tsx
// app/api/route.ts
export const runtime = "edge"
export async function GET() {
return new Response("Hello from Edge!")
}
// app/page.tsx
export const runtime = "edge"
export default async function Page() {
return <div>Edge-rendered page</div>
}Quick Reference
| Capability | Server Component | Client Component |
|---|---|---|
| Data fetching | ✅ Yes | ⚠️ Use SWR/React Query |
| Backend access | ✅ Yes (DB, files) | ❌ No |
| Event handlers | ❌ No | ✅ Yes |
| State/Effects | ❌ No | ✅ Yes |
| Browser APIs | ❌ No | ✅ Yes |
| Bundle size | 0 KB | Adds to bundle |
| Streaming | ✅ Yes | ❌ No |
Best Practices
- Default to Server Components - Only use 'use client' when needed
- Move Client Components down - Push them to leaves of component tree
- Pass data down - Fetch in Server Components, pass to Client Components
- Use composition - Nest Server Components inside Client Components via children
- Cache expensive operations - Use React cache() for deduplication