Skip to main content

server-client-components

Server and Client Components in the Next.js App Router — where to put 'use client', which props can cross the boundary, passing server components as children, context providers, wrapping third-party components, and keeping server-only code out of the client bundle with the server-only and client-only packages. Use when deciding or fixing component boundaries.

Source facts

Repository
Agents-Store/codex-plugins
Last source activity
November 1, 2026 at 02:46
Detected SKILL.md language
English
Stars
0
Forks
0

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

File Explorer
3 files

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
server-client-components
description
Server and Client Components in the Next.js App Router — where to put 'use client', which props can cross the boundary, passing server components as children, context providers, wrapping third-party components, and keeping server-only code out of the client bundle with the server-only and client-only packages. Use when deciding or fixing component boundaries.
# Server and Client Components In the App Router, all components are Server Components by default. Client Components are opted into with the `'use client'` directive. Understanding when to use each and how to compose them is essential for building performant Next.js apps. ## Decision Matrix Use **Server Components** when you need to: - Fetch data from databases or APIs close to the source - Use API keys, tokens, and secrets without exposing them to the client - Reduce the amount of JavaScript sent to the browser - Improve First Contentful Paint and stream content progressively Use **Client Components** when you need to: - Interactivity — `onClick`, `onChange`, form state - React state — `useState`, `useReducer` - Lifecycle effects — `useEffect` - Browser APIs — `localStorage`, `window`, `navigator.geolocation` - Custom hooks that use state or effects ## The `'use client'` Boundary Add `'use client'` at the top of a file to mark it as a Client Component: ```tsx 'use client' import { useState } from 'react' export default function Counter() { const [count, setCount] = useState(0) return <button onClick={() => setCount(count + 1)}>{count}</button> } ``` `'use client'` declares a boundary between Server and Client module graphs. Once a file has `'use client'`, all its imports and child components become part of the client bundle. Do not add the directive to every interactive component — add it at the highest boundary needed. ## Minimize Client Boundaries Push `'use client'` as far down the component tree as possible. Keep layouts and data-fetching components as Server Components: ```tsx // app/layout.tsx — Server Component (no 'use client') import Search from './search' import Logo from './logo' export default function Layout({ children }: { children: React.ReactNode }) { return ( <> <nav> <Logo /> {/* Server Component — no JS sent */} <Search /> {/* Client Component — interactive */} </nav> <main>{children}</main> </> ) } ``` ```tsx // app/search.tsx 'use client' export default function Search() { // Uses state, event handlers — needs client return <input type="search" onChange={/* ... */} /> } ``` This pattern keeps the majority of the layout server-rendered and only ships JavaScript for the interactive search bar. ## Passing Data: Server to Client Pass data from Server Components to Client Components via props. Props must be serializable (no functions, classes, or Dates without conversion): ```tsx // app/[id]/page.tsx — Server Component import LikeButton from './like-button' import { getPost } from '@/lib/data' export default async function Page({ params }: { params: Promise<{ id: string }> }) { const { id } = await params const post = await getPost(id) // Pass only the serializable data the Client Component needs return <LikeButton postId={post.id} initialLikes={post.likes} /> } ``` ```tsx // app/[id]/like-button.tsx — Client Component 'use client' import { useState } from 'react' export default function LikeButton({ postId, initialLikes }: { postId: string; initialLikes: number }) { const [count, setCount] = useState(initialLikes) return <button onClick={() => setCount(c => c + 1)}>{count} likes</button> } ``` Be explicit about what crosses the boundary — pass specific primitive props (strings, numbers) rather than entire objects when possible. This makes the serialization contract clear and keeps the Client Component lightweight. ## Interleaving: Server Components Inside Client Components Pass Server Components as `children` or other props to Client Components: ```tsx // app/ui/modal.tsx — Client Component 'use client' import { useState } from 'react' export default function Modal({ children }: { children: React.ReactNode }) { const [isOpen, setIsOpen] = useState(false) return ( <> <button onClick={() => setIsOpen(true)}>Open</button> {isOpen && <div className="modal">{children}</div>} </> ) } ``` ```tsx // app/page.tsx — Server Component import Modal from './ui/modal' import Cart from './ui/cart' // Server Component that fetches data export default function Page() { return ( <Modal> <Cart /> {/* Rendered on server, passed as children */} </Modal> ) } ``` The `<Cart />` Server Component renders on the server. The `<Modal>` Client Component receives its rendered output as `children`. This pattern avoids pulling data-fetching logic into the client bundle. ## Context Providers React context requires `'use client'`. Create a wrapper Client Component and use it in a Server Component layout: ```tsx // app/providers.tsx 'use client' import { ThemeProvider } from 'next-themes' import { SessionProvider } from 'next-auth/react' // Auth.js v5 only — Better Auth needs no provider (use authClient.useSession()) export default function Providers({ children }: { children: React.ReactNode }) { return ( <SessionProvider> <ThemeProvider attribute="class" defaultTheme="system"> {children} </ThemeProvider> </SessionProvider> ) } ``` ```tsx // app/layout.tsx — Server Component import Providers from './providers' export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en"> <body> <Providers>{children}</Providers> </body> </html> ) } ``` Render providers as deep as possible in the tree. Wrap only `{children}`, not the entire `<html>` document, so Next.js can optimize the static parts. ## Third-Party Component Wrapping Third-party components that use client features but lack `'use client'` must be wrapped: ```tsx // app/carousel.tsx 'use client' import { Carousel } from 'acme-carousel' export default Carousel ``` Now `<Carousel />` can be used inside Server Components: ```tsx // app/page.tsx — Server Component import Carousel from './carousel' export default function Page() { return <Carousel /> } ``` ## Environment Poisoning Prevention Every data-access module that touches databases, API keys, or secrets should start with `import 'server-only'`. This causes a build-time error if the module is accidentally imported in a Client Component — catching the leak before it reaches production: ```tsx // lib/data.ts import 'server-only' // Build fails if any Client Component imports this file export async function getData() { const res = await fetch('https://api.example.com/data', { headers: { authorization: process.env.API_KEY! }, }) return res.json() } ``` ```tsx // lib/products.ts import 'server-only' export async function getProduct(slug: string) { /* ... */ } export async function getAllProducts() { /* ... */ } ``` Install both guard packages: ```bash npm install server-only client-only ``` Use `import 'client-only'` in modules that depend on browser APIs (`localStorage`, `window`) to prevent accidental server-side usage. Only env vars prefixed with `NEXT_PUBLIC_` are included in the client bundle. Unprefixed variables are replaced with empty strings on the client. Never put secrets in `NEXT_PUBLIC_*` variables. ## Shared Types Across Server/Client Boundary When Server and Client Components share types, extract them into a standalone types file. Type-only files have no runtime code, so they don't affect bundling: ```tsx // types/product.ts — shared between server and client export interface Product { id: string name: string slug: string price: number image: string } export interface CartItem { product: Product quantity: number } ``` ```tsx // lib/products.ts — Server-only data layer import 'server-only' import type { Product } from '@/types/product' export async function getProduct(slug: string): Promise<Product | null> { /* ... */ } ``` ```tsx // components/add-to-cart.tsx — Client Component 'use client' import type { Product } from '@/types/product' export function AddToCartButton({ product }: { product: Product }) { /* ... */ } ``` The `import type` syntax ensures types are stripped at build time and never affect bundle size. ## Common Mistakes | Mistake | Fix | |---------|-----| | Adding `'use client'` to the root layout | Move interactive parts to child Client Components | | Importing a Server Component into a Client Component file | Pass as `children` prop instead | | Using `useEffect` for data fetching | Fetch in Server Components or use Server Actions | | Passing non-serializable props (functions, Date objects) to Client Components | Serialize data before passing; move callbacks to Client Components | | Adding `'use client'` to every component | Only add at the boundary — child components inherit client status |
View on GitHub