Introduction π
The Server and Client Component model is the foundation of the Next.js App Router. It lets you choose, at the component level, whether code runs on the server, the client, or both β unlocking smaller bundles, direct backend access, and fine-grained interactivity. This tutorial covers the mental model, the rules, and the practical patterns for working with both.
Information
What are Server Components? π₯οΈ
Server Components render exclusively on the server. Their code β including any imported libraries β is never sent to the browser, which keeps client bundles small. They can directly access backend resources like databases, file systems, or internal APIs.
app/posts/page.tsx
export default async function Posts() {
const posts = await db.query("SELECT * FROM posts");
return <List type="unordered">{posts.map((p) => <List.Item key={p.id}>{p.title}</List.Item>)}</List>;
}Note
What are Client Components? π±οΈ
Client Components render on the server for the initial HTML, then hydrate in the browser to become interactive. They support state, effects, event handlers, and browser-only APIs.
components/Counter.tsx
"use client";
import { useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>Count: {count}</button>;
}Server Components vs Client Components βοΈ
| Feature | Server Component | Client Component |
|---|---|---|
| Direct database/API access | β | β |
| useState / useReducer | β | β |
| Event handlers | β | β |
| Browser APIs (window, localStorage) | β | β |
| Ships JS to the browser | β | β |
| Async/await component body | β | β |
React Server Components π§
React Server Components are a React architecture, not a Next.js-specific feature β Next.js is one of the first frameworks to implement them fully. RSC output isn't plain HTML; it's a special serialized format called the RSC Payload, which React uses to reconcile the UI.
The "use client" Directive π·οΈ
Adding "use client" at the top of a file marks it β and everything it imports β as part of the client module graph. This is a boundary marker, not a per-component switch.
components/SearchBox.tsx
"use client";
import { useState } from "react";
export default function SearchBox() {
const [query, setQuery] = useState("");
return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
}Important
The "use server" Directive π·οΈ
"use server" marks a function (or entire file) as a Server Action β code that runs on the server but can be called directly from Client Components, commonly used for mutations like form submissions.
app/actions.ts
"use server";
export async function createPost(formData: FormData) {
const title = formData.get("title");
await db.insert({ title });
}Component Boundaries π§
A boundary is created wherever "use client" appears. Everything imported below that file β even components without their own directive β becomes part of the client bundle.
Warning
Rendering Flow π
On the initial request, Server Components render first, producing an RSC Payload that includes placeholders for Client Components. The server then renders everything to HTML, and the browser hydrates only the Client Component portions.
- Server renders Server Components to the RSC Payload.
- Client Components are included as references, with their props serialized.
- The full tree is rendered to HTML and sent to the browser.
- React hydrates only the Client Component portions.
Data Fetching in Server Components π‘
Server Components can be declared async and use await directly in the component body β no useEffect or loading state management required.
app/products/page.tsx
export default async function Products() {
const res = await fetch("https://api.example.com/products");
const products = await res.json();
return <List type="unordered">{products.map((p) => <List.Item key={p.id}>{p.name}</List.Item>)}</List>;
}Data Fetching in Client Components π‘
Client Components fetch data using traditional React patterns: useEffect combined with useState, or a data-fetching library like SWR or React Query.
components/LiveTicker.tsx
"use client";
import { useEffect, useState } from "react";
export default function LiveTicker() {
const [price, setPrice] = useState<number | null>(null);
useEffect(() => {
const id = setInterval(async () => {
const res = await fetch("/api/price");
setPrice(await res.json());
}, 5000);
return () => clearInterval(id);
}, []);
return <p>Price: {price ?? "Loadingβ¦"}</p>;
}Passing Data Between Components π
Server Components can pass data down to Client Components as props, as long as that data is serializable. The reverse β passing functions or class instances from server to client β is not supported.
app/page.tsx
import LikeButton from "./LikeButton";
export default async function Page() {
const post = await getPost();
return <LikeButton initialLikes={post.likes} />;
}Serialization Rules π¦
Props passed from Server to Client Components must be serializable β plain objects, arrays, strings, numbers, booleans, and a few special types. Functions, classes, and Symbols are not allowed, with the exception of Server Actions.
| Type | Serializable? |
|---|---|
| String, Number, Boolean | β |
| Plain Objects / Arrays | β |
| Date | β |
| Server Actions (functions) | β (special case) |
| Regular functions / closures | β |
| Class instances | β |
Interactivity in Client Components β‘
Only Client Components can respond to user interaction through event handlers like onClick, onChange, and onSubmit, since these require JavaScript running in the browser.
components/Toggle.tsx
"use client";
import { useState } from "react";
export default function Toggle() {
const [open, setOpen] = useState(false);
return <button onClick={() => setOpen(!open)}>{open ? "Hide" : "Show"}</button>;
}Browser APIs π
APIs like window, document, localStorage, and navigator only exist in the browser, so any code using them must live inside a Client Component.
components/ThemeToggle.tsx
"use client";
import { useEffect, useState } from "react";
export default function ThemeToggle() {
const [theme, setTheme] = useState("light");
useEffect(() => {
setTheme(localStorage.getItem("theme") ?? "light");
}, []);
return <p>Current theme: {theme}</p>;
}Server-Only Code π
Code that should never reach the client β like secrets or direct database queries β can be explicitly protected using the server-only package, which throws a build error if accidentally imported into a Client Component.
lib/db.ts
import "server-only";
export async function getSecretData() {
return db.query("SELECT * FROM secrets");
}Client-Only Code π
Conversely, the client-only package guards code that relies on browser APIs, throwing an error if it's accidentally imported into a Server Component.
lib/analytics.ts
import "client-only";
export function trackEvent(name: string) {
window.dataLayer?.push({ event: name });
}Shared Components π€
Components with no server- or client-only dependencies β like a simple <Button> β can be used in either context and will render according to whichever boundary imports them.
components/Button.tsx
export default function Button({ children }: { children: React.ReactNode }) {
return <button>{children}</button>;
}Component Composition π§©
A common and powerful pattern is passing Server Components into Client Components via the children prop. This lets server-rendered content live inside a client boundary without itself becoming a Client Component.
components/Modal.tsx
"use client";
export default function Modal({ children }: { children: React.ReactNode }) {
return <div className="modal">{children}</div>;
}app/page.tsx
import Modal from "@/components/Modal";
import ServerContent from "@/components/ServerContent";
export default function Page() {
return (
<Modal>
<ServerContent />
</Modal>
);
}Tip
Nesting Server and Client Components πͺ
Server Components can render Client Components, but Client Components cannot directly import Server Components β they can only receive them as children or other props from above.
Third-Party Libraries π
Many third-party components rely on hooks, context, or browser APIs but don't include their own "use client" directive. Wrapping them in your own Client Component file resolves this.
components/CarouselWrapper.tsx
"use client";
import Carousel from "some-carousel-library";
export default function CarouselWrapper(props: any) {
return <Carousel {...props} />;
}Server Actions Overview π οΈ
Server Actions are asynchronous functions marked with "use server" that run on the server but can be invoked directly from Client Components β commonly used for form submissions and mutations, without manually building an API route.
app/actions.ts
"use server";
export async function subscribe(formData: FormData) {
const email = formData.get("email");
await db.subscribers.create({ email });
}components/SubscribeForm.tsx
import { subscribe } from "@/app/actions";
export default function SubscribeForm() {
return (
<form action={subscribe}>
<input name="email" type="email" />
<button type="submit">Subscribe</button>
</form>
);
}Performance Considerations π
- Every "use client" boundary adds to the client JavaScript bundle β use them sparingly.
- Fetch data as high up the tree as possible in Server Components to avoid client-side waterfalls.
- Push state and interactivity into small, focused Client Components rather than large ones.
Bundle Size Optimization π¦
Keeping Client Components small and leaf-like in the component tree minimizes the JavaScript shipped to the browser, since only code below a "use client" boundary is bundled for the client.
Best Practice
SEO Considerations π
Because both Server and Client Components produce HTML on the initial server render, content in either is crawlable β but relying on Server Components for primary content avoids any risk from client-side rendering delays or JS execution failures.
Migration Strategies π
When migrating an existing client-heavy app to the Server Component model, a common approach is to start with everything as Server Components and add "use client" only where errors surface β such as missing hooks or event handlers.
- Start migration at leaf components (buttons, inputs, small widgets).
- Move data fetching from useEffect into async Server Components.
- Replace API routes used purely for mutations with Server Actions where appropriate.
Best Practices β
- Default to Server Components; reach for "use client" only when interactivity is genuinely needed.
- Pass Server Components as children into Client Components instead of importing them directly.
- Guard sensitive code with the server-only package to catch accidental client leaks early.
- Keep Client Component boundaries small and focused to minimize bundle size.
Common Mistakes β οΈ
- Adding "use client" to an entire page instead of just the interactive piece.
- Trying to import a Server Component directly inside a Client Component.
- Passing non-serializable props (like functions) from a Server Component to a Client Component.
- Forgetting that "use client" marks a boundary, not just a single component.