Overview
In the Next.js App Router, layouts and pages are Server Components by default: they render on the server, can read databases and secrets directly, and send no component JavaScript to the browser. Migration means moving interactivity into small Client Components and moving data fetching up to the server. Rules and trade-offs: react-server-components and server-vs-client-components.
Prerequisites
- A Next.js project on the App Router. Pages Router code must first move to
app/; Server Components exist only there. The current Next.js docs describe version 16. - A green build and test run to compare against.
Steps
1. List what needs the client
A component must be a Client Component if it uses state or effects, event handlers (onClick, onChange), custom hooks that use either, or browser-only APIs (window, localStorage).
grep -rlE "useState|useEffect|useReducer|onClick|onChange|window\." src components appEverything not listed is a candidate to stay on the server.
2. Push "use client" down
The directive marks a boundary: the file and everything it imports and renders directly joins the client bundle. Put it on the smallest interactive component, not on the page.
// app/products/[id]/page.tsx (Server Component, no directive)
import { db } from "@/lib/db";
import AddToCart from "@/components/AddToCart";
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params; // params is a Promise in current Next.js
const product = await db.product.findUnique({ where: { id } });
if (!product) return null;
return (
<main>
<h1>{product.name}</h1>
<AddToCart productId={product.id} price={product.price} />
</main>
);
}// components/AddToCart.tsx
"use client";
import { useState } from "react";
export default function AddToCart({ productId, price }: { productId: string; price: number }) {
const [qty, setQty] = useState(1);
/* interactive UI only */
}Props crossing the boundary must be serializable.
3. Fetch on the server
Replace useEffect plus fetch plus a spinner with an async component that awaits the data, and put a loading.tsx or a <Suspense> boundary around slow parts.
export async function UserProfile({ userId }: { userId: string }) {
const user = await db.user.findUnique({ where: { id: userId } });
return <div>{user?.name}</div>;
}4. Handle context and shared state
Server Components cannot read React context. Wrap providers in a small Client Component that accepts children, and render it in a Server Component layout, as deep in the tree as possible:
"use client";
export default function ThemeProvider({ children }: { children: React.ReactNode }) {
return <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>;
}Pass Server Components into Client Components as children or other props; do not import them from a Client Component file, or they become client code.
5. Keep secrets on the server
Add import "server-only" to modules that read secrets or the database. Importing one into a Client Component then fails the build, and only NEXT_PUBLIC_ variables are ever sent to the browser.
6. Measure the bundle
npx next experimental-analyze # Turbopack analyzer, Next.js 16.1 and later
# webpack builds: install @next/bundle-analyzer, then ANALYZE=true npm run buildFilter by client environment and confirm data libraries (SWR, React Query, ORM clients, pg) no longer appear in client chunks.
Verify it worked
next build && next startThe build passes, view-source shows content in the HTML, and the analyzer shows fewer client modules than before.
Common errors
- “You’re importing a component that needs
useState”. A hook or event handler sits in a Server Component; extract the interactive part into a Client Component. - A “server-only” import fails in a client bundle. The import chain reaches a Client Component; follow it with the analyzer’s import trace.
- Context is
undefinedon the server. Server Components cannot consume it; pass props or read it in a client wrapper. - A query runs on every request. Async server fetches are not cached by default in current Next.js; deduplicate within a request with React
cache(), or opt in to caching with theuse cachedirective (enabled bycacheComponents: true). paramsis a Promise. Await it, as in step 2.
FAQ
Which components have to stay Client Components?
Any component that uses state or effects, event handlers such as onClick, custom hooks that use either, or browser-only APIs such as window and localStorage. Everything else can stay on the server.
Where should I put “use client”?
On the smallest interactive component, not on the page, because the file and everything it imports and renders directly joins the client bundle.
Can Server Components use React context?
No. Wrap providers in a small Client Component that accepts children, and render it from a Server Component layout as deep in the tree as possible.
Are fetches in Server Components cached by default in Next.js?
No. Async server fetches are not cached by default in current Next.js; deduplicate within a request with React cache(), or opt in with the use cache directive.
How do I stop secrets from leaking into the client bundle?
Add import "server-only" to modules that read secrets or the database, so importing one into a Client Component fails the build.