Technology · Next.js
Next.js Server Components
A practical guide to server components, the use client directive, and rendering boundaries.
TL;DR
- 01Server components run on the server and send HTML to the browser.
- 02Use client components with the "use client" directive for interactivity.
- 03Mix both types to optimize performance and security.
Server Components Basics
- Server components are the default in the App Router.
// app/page.tsx - server component by default export default async function Page() { const data = await fetch('https://api.example.com/data'); return <div>{data}</div>; } - Fetch data directly inside the component without useEffect.
- Access databases and secrets safely since code never reaches the browser.
- Return HTML that is sent to the client for instant page load.
- Cannot use hooks like useState or event listeners in server components.
Client Components
- Add
"use client"at the top of a file to make it a client component."use client"; import { useState } from "react"; export default function Counter() { const [count, setCount] = useState(0); return <button onClick={() => setCount(count + 1)}>{count}</button>; } - Client components run in the browser and can use all React hooks.
- Use them only for interactive features that need state or event handlers.
- They still benefit from server-side rendering, just with client hydration.
- Keep client components small to minimize the JavaScript bundle.
Mixing Server and Client
- Pass server component data to a client component as a prop.
// Server component export default async function Page() { const data = await fetchData(); return <ClientComponent data={data} />; } // Client component "use client"; export function ClientComponent({ data }) { const [count, setCount] = useState(0); return <div>{data}</div>; } - Server components can wrap client components and pass props down.
- This pattern keeps sensitive code on the server and interactivity on the client.
- Use server components as high-level wrappers for data fetching.
- Pass serializable values only — functions and class instances cannot cross the boundary.
// OK: strings, numbers, plain objects, arrays <ClientComponent name={user.name} count={42} items={["a", "b"]} /> // Not OK: functions defined in server components // <ClientComponent onClick={serverFn} /> // will error
Server-Only Code
- Use the
"use server"directive to mark functions that run only on the server."use server"; export async function deletePost(id: number) { await db.delete('posts', id); } - Call server functions from client components like normal functions.
"use client"; import { deletePost } from "./actions"; export default function Post({ id }) { return <button onClick={() => deletePost(id)}>Delete</button>; } - Server functions are type-safe and automatically serialized over the network.
- Use them for mutations and sensitive operations that should not expose logic.
- Import
server-onlypackage to prevent accidental client-side imports.import "server-only"; // throws a build error if imported in a client component export async function getSecretData() { return await db.query("SELECT * FROM secrets"); }
Performance Best Practices
- Keep server components at the top of the tree for maximum data fetching.
- Move client components down as low as possible in the component tree.
- Cache expensive server queries using
fetchcaching orunstable_cache.const data = await fetch('https://...', { next: { revalidate: 3600 } }); - Use streaming with Suspense to show loading states while data fetches.
<Suspense fallback={<p>Loading...</p>}> <ChildComponent /> </Suspense> - Avoid passing large objects or functions to client components as props.
Tip: Keep the "use client" boundary as low as possible in the tree to maximize server rendering and minimize the client bundle size.
Warning: Never include environment secrets in client components, even as props, since they become visible in the browser bundle and HTML.