Suggest an editImprove this articleRefine the answer for “Server and client component patterns in Next.js”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Server and client component patterns** define which parts of a Next.js app render on the server and which hydrate in the browser. ```tsx // Server Component - fetches data, ships zero JS async function Page() { const data = await db.getPosts(); return <PostGrid posts={data} />; // HTML only } // PostGrid can render a Client Component for interactive filtering ``` **Key rule:** default to Server Components; add `"use client"` only at interactive leaves of the component tree.Shown above the full answer for quick recall.Answer (EN)Image**Server and client component patterns** in Next.js split rendering work between Node.js and the browser, letting you ship less JavaScript while keeping interactivity exactly where the UI needs it. ## Theory ### TL;DR - Server Components render on the server and send HTML - no JS bundle shipped for that file - Client Components (marked `"use client"`) hydrate in the browser and handle state, events, browser APIs - Default to Server Components; add `"use client"` only where hooks or DOM interaction are needed - Push Client Components as far down the tree as possible - a small leaf, not a whole page - Server Components can wrap Client Components via `children`; Client Components cannot import Server Components directly ### Quick example ```tsx // app/page.tsx - Server Component, fetches data directly on the server async function Page() { const posts = await fetch('https://api.example.com/posts').then(r => r.json()); return <PostList posts={posts} />; // Sends HTML, zero client JS for this file } // components/PostItem.tsx - Client Component, handles likes 'use client'; import { useState } from 'react'; function PostItem({ post }: { post: Post }) { const [likes, setLikes] = useState(0); return ( <li> {post.title} <button onClick={() => setLikes(likes + 1)}>👍 {likes}</button> </li> ); } // Server renders the full list as HTML. Client hydrates only the like buttons. ``` The server handles data fetching and initial HTML. The client takes over only at the button. ### When to use each - Fetch data or access DB: Server Component - direct access, no secrets exposed to the browser - Static content, SEO: Server Component - pre-rendered HTML for crawlers - `useState`, `useEffect`, other hooks: Client Component - browser-only React features - Event handlers (`onClick`, `onChange`): Client Component - needs the DOM - Browser APIs (`localStorage`, `window`, `document`): Client Component - server has no browser context - Minimize JS bundle: Server Component - no hydration JS shipped ### Comparison | Aspect | Server Component | Client Component | |--------|-----------------|------------------| | Execution | Node.js (SSR/RSC) | Browser (hydration) | | JS bundle | None shipped | Full JS + hydration | | Data fetching | Direct `async/await`, DB | `fetch()`, SWR, TanStack Query | | Interactivity | None | Full (state, events) | | Children | Can include Client Components | Cannot import Server Components | | Secrets | Safe in server env vars | Risky - exposed in bundle | | Typical use | Dashboard with DB data, SEO blog | Forms, modals, toggle menus | ### Pattern 1: Push Client components to the leaves Move `"use client"` to the smallest possible piece of the UI. A blog post page is mostly static HTML - only the share button needs the browser. ```tsx // app/blog/[slug]/page.tsx - Server Component export default async function BlogPost({ params }: { params: Promise<{ slug: string }> }) { const { slug } = await params; const post = await getPost(slug); // Direct DB call, no API key exposed return ( <article> <h1>{post.title}</h1> <p>{post.content}</p> {/* Static HTML, no JS shipped */} <ShareButton url={post.url} /> {/* Client boundary lives here */} </article> ); } // components/ShareButton.tsx - Client Component 'use client'; export function ShareButton({ url }: { url: string }) { return <button onClick={() => navigator.share({ url })}>Share</button>; } ``` Compare that to putting `"use client"` on the whole `BlogPost` page. You force a `useEffect` fetch cycle, ship the full component tree as JS, and lose server-rendered HTML for SEO. I've seen this mistake drop Lighthouse scores from 90+ to below 60 on content-heavy sites. ### Pattern 2: Server Components as children A Client Component can accept Server Components as `children`. The wrapper renders on the client; the children still run on the server. The key: pass them from a Server parent, not import them inside the Client Component. ```tsx // components/Accordion.tsx - Client Component 'use client'; import { useState } from 'react'; export function Accordion({ title, children }: { title: string; children: React.ReactNode }) { const [isOpen, setIsOpen] = useState(false); return ( <div> <button onClick={() => setIsOpen(!isOpen)}>{title}</button> {isOpen && <div>{children}</div>} </div> ); } // app/page.tsx - Server Component export default async function Page() { const data = await fetchData(); // Runs on server return ( <Accordion title="Details"> <ServerContent data={data} /> {/* Renders on server, passed as children */} </Accordion> ); } ``` Shadcn/UI ships its interactive wrappers this way: Client shells that accept server-rendered content as slots. ### Pattern 3: Fetch on server, interact on client Fetch the full dataset in a Server Component and pass it as serializable props. Let the Client Component handle in-memory filtering, sorting, or pagination - no extra network round trips. ```tsx // app/products/page.tsx - Server Component export default async function ProductsPage() { const products = await getProducts(); // One server-side call return <ProductGrid products={products} />; } // components/ProductGrid.tsx - Client Component 'use client'; export function ProductGrid({ products }: { products: Product[] }) { const [filter, setFilter] = useState(''); const filtered = products.filter(p => p.name.includes(filter)); return ( <div> <input value={filter} onChange={e => setFilter(e.target.value)} /> {filtered.map(p => <ProductCard key={p.id} product={p} />)} </div> ); } ``` Props crossing the server-client boundary must be serializable: strings, numbers, plain objects, arrays. Functions cannot cross that line - that is where Server Actions come in. ### Pattern 4: Context Providers React Context requires a Client Component. Wrap providers in a separate file and import them from a Server layout. The children remain server-rendered because they are passed in from outside the Client boundary. ```tsx // app/providers.tsx - Client Component 'use client'; import { ThemeProvider } from 'next-themes'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; const queryClient = new QueryClient(); export function Providers({ children }: { children: React.ReactNode }) { return ( <ThemeProvider> <QueryClientProvider client={queryClient}> {children} </QueryClientProvider> </ThemeProvider> ); } // app/layout.tsx - Server Component import { Providers } from './providers'; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html> <body> <Providers> {children} {/* Still Server Components */} </Providers> </body> </html> ); } ``` ### Common mistakes **Fetching data inside a Client Component when a Server parent would do:** ```tsx // Bad - double fetch, bundle bloat, flash of empty content 'use client'; export function PostList() { const [posts, setPosts] = useState([]); useEffect(() => { fetch('/api/posts').then(r => r.json()).then(setPosts); }, []); return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>; } // Good - fetch in a Server parent, pass as props export default async function Page() { const posts = await fetch('https://api.example.com/posts').then(r => r.json()); return <PostList posts={posts} />; // PostList just renders, no useEffect needed } ``` **Adding `"use client"` to the root layout:** ```tsx // Bad - forces the entire app to ship as client JS // app/layout.tsx 'use client'; // Everything below now hydrates ``` Keep root layouts as Server Components. Extract only the parts that need client features into their own files. **Passing a function as a prop from Server to Client:** ```tsx // Bad - functions are not serializable across the RSC boundary export default function Server() { return <ClientComp onClick={() => console.log('hello')} />; // Runtime error } // Good - define the handler inside the Client Component 'use client'; export function ClientComp() { return <button onClick={() => console.log('hello')}>Click</button>; } ``` For server-side mutations triggered by a client event, use a Server Action: mark the function `'use server'` and attach it to a form `action` prop. **Using browser APIs in a Server Component:** ```tsx // Bad - server has no window or localStorage export default function Theme() { localStorage.setItem('theme', 'dark'); // TypeError at runtime on Vercel } ``` Move any `window`, `localStorage`, or `document` calls into Client Components. ### Real-world usage - Vercel Dashboard: Server Components fetch deployment data from the DB; toggle switches and dropdowns are Client Components - Shadcn/UI: interactive components (buttons, dialogs, dropdowns) carry `"use client"`, placed inside Server pages - NextAuth apps: session checks run in Server Components via `auth()`; the user avatar dropdown is a Client Component - Payload CMS: admin data tables are server-rendered; the Lexical rich text editor is a Client Component - Next.js 14+ apps with Server Actions: mutations go through `'use server'` functions; everything else stays server-rendered ### Follow-up questions **Q:** What happens if you import a Server Component inside a Client Component? **A:** Next.js throws an error. Client Components cannot import Server Components directly. Use the `children` prop pattern - pass the Server Component from a Server parent so it is already rendered before the Client Component runs. **Q:** Can a Client Component fetch data? **A:** Yes, with `useEffect` plus `fetch`, SWR, or TanStack Query. Avoid it for initial page data though - you get a second network round trip and a flash of empty content that a Server Component fetch prevents entirely. **Q:** What is the RSC payload and how does it differ from HTML? **A:** The server sends a binary RSC stream alongside the initial HTML. It contains component props and references so the browser's React reconciler can hydrate Client subtrees without re-fetching data. The client merges both streams silently. **Q:** How do Server Actions differ from API routes? **A:** Server Actions are functions marked `'use server'` that run on the server when triggered by a form or client event. They work without JavaScript enabled (progressive enhancement). API routes are separate HTTP endpoints that always require an explicit `fetch` call. **Q:** (Senior) Why can Server Components be passed as `children` to Client Components but not imported inside them? **A:** When passed as `children`, Server Components are already rendered to the RSC payload by the server before the Client Component executes. The Client Component receives an opaque reference, not the component function, so no boundary is violated. A direct import would pull the Server Component into the client bundle, which React forbids. ## Examples ### Dashboard with server data and a client download action ```tsx // app/dashboard/page.tsx - Server Component // Direct DB access - no API key exposure, no extra fetch wrapper needed export default async function Dashboard() { const metrics = await db.query('SELECT * FROM metrics ORDER BY date DESC LIMIT 30'); return ( <main> <h1>Dashboard</h1> <StatsGrid metrics={metrics} /> {/* Server: plain HTML table */} <ExportButton data={metrics} /> {/* Client: uses Browser File API */} </main> ); } // components/ExportButton.tsx - Client Component 'use client'; export function ExportButton({ data }: { data: Metric[] }) { const handleExport = () => { const blob = new Blob([JSON.stringify(data)], { type: 'application/json' }); const url = URL.createObjectURL(blob); // Browser-only API const link = document.createElement('a'); link.href = url; link.download = 'metrics.json'; link.click(); }; return <button onClick={handleExport}>Export JSON</button>; } // Server streams the full dashboard as HTML. Browser hydrates only the export button. ``` ### Blog post with a client comment section This follows the pattern from the Next.js docs. Server streams article content; comments are a client island. ```tsx // app/blog/[slug]/page.tsx - Server Component import { getPost } from '@/lib/posts'; import { CommentSection } from './CommentSection'; export default async function BlogPost({ params }: { params: Promise<{ slug: string }> }) { const { slug } = await params; const post = await getPost(slug); // DB call on server, secrets never exposed return ( <article> <h1>{post.title}</h1> <div dangerouslySetInnerHTML={{ __html: post.content }} /> <CommentSection postId={post.id} /> </article> ); } // app/blog/[slug]/CommentSection.tsx - Client Component 'use client'; import { useState } from 'react'; export function CommentSection({ postId }: { postId: string }) { const [comments, setComments] = useState<{ id: string; text: string }[]>([]); const [input, setInput] = useState(''); const handleAdd = () => { if (!input.trim()) return; setComments(prev => [...prev, { id: Date.now().toString(), text: input }]); setInput(''); }; return ( <section> <h2>Comments</h2> <input value={input} onChange={e => setInput(e.target.value)} placeholder="Add a comment" /> <button onClick={handleAdd}>Post</button> <ul>{comments.map(c => <li key={c.id}>{c.text}</li>)}</ul> </section> ); } // Article HTML is server-rendered for SEO. Comment section hydrates independently. ``` ### Server Action for a delete mutation Functions cannot be passed as props from Server to Client. Server Actions solve this without API routes. ```tsx // app/posts/actions.ts - Server Action 'use server'; import { db } from '@/lib/db'; export async function deletePost(formData: FormData) { const id = formData.get('id') as string; await db.posts.delete({ where: { id } }); } // app/posts/PostList.tsx - Client Component 'use client'; import { deletePost } from './actions'; export function PostList({ posts }: { posts: Post[] }) { return ( <ul> {posts.map(post => ( <li key={post.id}> {post.title} <form action={deletePost}> <input type="hidden" name="id" value={post.id} /> <button type="submit">Delete</button> </form> </li> ))} </ul> ); } // deletePost serializes data via FormData and runs server-side. // Works without JavaScript enabled - progressive enhancement. ```For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.