Documentation

Server vs Client Components

Decision rules for server vs client components, the providers boundary, and passing server data to client components.

Show:

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 useState or useReducer for 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

FileServer or ClientReason
app/layout.tsx (root)ServerHTML shell, metadata, no hooks
app/providers.tsxClientQueryClientProvider requires context
app/(app)/layout.tsxClientuseSession, useRouter
app/(app)/dashboard/page.tsxClientUses React Query hooks
app/page.tsx (landing)ServerStatic content, no interactivity
app/api/**/route.tsServerAPI route handler
components/ui/Button.tsxClientEvent handlers (onClick)
components/ui/Card.tsxServerPurely 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 a Suspense boundary or passing as children.
  • API routes (app/api/**/route.ts) are always server-side — they can use process.env secrets safely.
  • localStorage, sessionStorage, and window are only available in client components. Guard access with typeof window !== 'undefined' when needed.