Server vs Client Components
Decision rules for server vs client components, the providers boundary, and passing server data to client components.
Framework implementations
In the App Router, all components are server components by default. Client components opt in with 'use client' at the top of the file.
Decision Rule
Use a server component when the component:
- Fetches data at render time using
async/await(not React Query) - Renders static or infrequently-changing content (marketing pages, MDX content)
- Does not use React hooks (
useState,useEffect,useQuery, etc.) - Does not use browser APIs (
localStorage,window, etc.) - Does not need event handlers attached to DOM elements
Use a client component ('use client') when the component:
- Uses any React hook
- Uses React Query for data fetching
- Needs
useStateoruseReducerfor local UI state - Handles user interaction (click, form submission)
- Uses
useRouter,useParams,useSearchParams - Uses browser APIs or third-party client libraries
In Practice: What Is Server vs Client
| File | Server or Client | Reason |
|---|---|---|
app/layout.tsx (root) | Server | HTML shell, metadata, no hooks |
app/providers.tsx | Client | QueryClientProvider requires context |
app/(app)/layout.tsx | Client | useSession, useRouter |
app/(app)/dashboard/page.tsx | Client | Uses React Query hooks |
app/page.tsx (landing) | Server | Static content, no interactivity |
app/api/**/route.ts | Server | API route handler |
components/ui/Button.tsx | Client | Event handlers (onClick) |
components/ui/Card.tsx | Server | Purely presentational, no handlers |
In a typical authenticated web app, most of the app is client components because pages use React Query hooks and layouts use auth hooks.
The Providers Boundary
The root layout is a server component. All client context providers live in app/providers.tsx which is 'use client'. This is the boundary:
// app/layout.tsx — SERVER COMPONENT
import { Providers } from './providers'
export default function RootLayout({ children }) {
return (
<html>
<body>
<Providers>{children}</Providers> {/* Providers is client, children can be server or client */}
</body>
</html>
)
}// app/providers.tsx — CLIENT COMPONENT
'use client'
import { QueryClientProvider, QueryClient } from '@tanstack/react-query'
import { useState } from 'react'
export function Providers({ children }) {
const [queryClient] = useState(() => new QueryClient({...}))
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
}children passed into a client component can still be server components — Next.js handles the boundary correctly.
Server Components for Static/Marketing Pages
For marketing sites or content-heavy pages, server components fetch data at request time without React Query:
// app/blog/[slug]/page.tsx — SERVER COMPONENT (no 'use client')
import { getPost } from '@/lib/cms'
export default async function BlogPostPage({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug) // Direct async data fetch
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
)
}This is appropriate for content that:
- Changes infrequently
- Does not need real-time updates
- Benefits from server-side rendering for SEO
Passing Server Data to Client Components
If a server component fetches data that a client component needs, pass it as props:
// app/products/page.tsx — SERVER
import { ProductList } from '@/components/products/ProductList'
import { getProducts } from '@/lib/data'
export default async function ProductsPage() {
const products = await getProducts() // Server-side fetch
return <ProductList initialProducts={products} /> // Pass to client
}// components/products/ProductList.tsx — CLIENT
'use client'
export function ProductList({ initialProducts }) {
// Can use initialProducts for immediate render,
// and React Query for subsequent updates
}Rules
- Never add
'use client'unnecessarily — keep components server-side when possible for performance. 'use client'propagates down: if a parent is a client component, all its children are also treated as client components.- Never import a client component (one with hooks or
'use client') into a server component without wrapping in aSuspenseboundary or passing aschildren. - API routes (
app/api/**/route.ts) are always server-side — they can useprocess.envsecrets safely. localStorage,sessionStorage, andwindoware only available in client components. Guard access withtypeof window !== 'undefined'when needed.