Server Components in Next.js
RSC mental model in the App Router — server vs client boundaries, data fetching, serialization rules, and composition patterns that stay maintainable.
- nextjs
- server-components
- rsc
- react
In the Next.js App Router, components are Server Components by default. They render on the server (at build or request time), can access backend resources directly, and ship minimal or zero interactive JS for their own logic. Client Components opt in with "use client" when you need state, effects, or browser-only APIs.
This page goes deep on boundaries and composition. For routing file conventions, see App Router overview.
Docs: Next.js Server Components, Client Components, React RSC.
What “server” means here
A Server Component:
- Runs in a Node (or edge) server environment during render — not in the browser.
- Can be
asyncandawaitdata. - Can import server-only modules (DB clients, secret env vars) if marked/structured correctly.
- Does not use
useState,useEffect, DOM APIs, or browser event handlers directly. - Serializes its output (and props to client children) across the wire as the RSC payload + HTML.
They are not “SSR of a classic client tree” only — the component model itself is split.
Server vs Client decision table
| Need | Prefer |
|---|---|
| Fetch data, read DB, use secrets | Server Component |
| Reduce JS shipped for static UI | Server Component |
useState / useReducer |
Client Component |
useEffect, subscriptions, browser APIs |
Client Component |
| onClick / onChange handlers | Client Component |
| Context that changes often on the client | Client Component |
| Custom hooks that use the above | Client Component module |
Rule: make the leaf interactive, keep parents as Server Components when possible.
// app/dashboard/page.tsx — Server Component
import { db } from '@/lib/db';
import { FilterBar } from './filter-bar'; // client
import { Chart } from './chart'; // server-friendly presentational
export default async function DashboardPage() {
const stats = await db.stats.overview();
return (
<main>
<FilterBar />
<Chart data={stats} />
</main>
);
}
// filter-bar.tsx
'use client';
import { useState } from 'react';
export function FilterBar() {
const [q, setQ] = useState('');
return (
<input
value={q}
onChange={(e) => setQ(e.target.value)}
aria-label="Filter"
/>
);
}
The client boundary
"use client" marks the file as the entry of a client bundle. That module and its imports become client-side, with exceptions for pure type imports and certain patterns.
Implications:
- Put
"use client"only in files that need it — not inpage.tsxif a child can own interactivity. - A Server Component can render a Client Component as a child.
- A Client Component cannot import a Server Component module directly.
- Pass Server Components as
childrenor props from a server parent into a client wrapper (composition pattern):
// client-shell.tsx
'use client';
export function ClientShell({ children }: { children: React.ReactNode }) {
return <div className="shell">{children}</div>;
}
// page.tsx — server
import { ClientShell } from './client-shell';
import { ServerList } from './server-list';
export default function Page() {
return (
<ClientShell>
<ServerList /> {/* still a Server Component */}
</ClientShell>
);
}
This “children as server slots” pattern is how you keep a client chrome without forcing the whole tree client-side.
Data fetching patterns
In Server Components
export default async function ProductPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const res = await fetch(`https://api.example.com/products/${id}`, {
next: { revalidate: 60 },
});
const product = await res.json();
return <ProductView product={product} />;
}
Colocate fetch with the component that needs the data. Use Suspense boundaries + loading.tsx so slow subtrees don’t block the whole page shell — good for LCP and perceived CRP.
Not “getServerSideProps”
There is no getServerSideProps in app/. Async components + caching options replace that mental model. Dynamic functions (cookies(), headers()) opt routes into dynamic rendering.
What can cross the boundary (props)
Props from Server → Client must be JSON-serializable (plus a few supported types like Date in some versions — stick to plain data).
// Bad: passing a function or class instance from server to client
<ClientWidget onSave={async () => db.save()} />
// Good: Server Action reference (supported pattern) or client fetch to a route
Server Actions ("use server") give you callable server functions from client forms/buttons — still validate auth on the server.
// actions.ts
'use server';
export async function updateName(formData: FormData) {
const name = String(formData.get('name') ?? '');
// authz + validation + mutation
}
// form.tsx
'use client';
import { updateName } from './actions';
export function NameForm() {
return (
<form action={updateName}>
<input name="name" />
<button type="submit">Save</button>
</form>
);
}
Never trust client input; treat actions like public POST endpoints.
Bundle and performance effects
Server Components aim to:
- Cut JS parse/hydrate costs → helps INP under load of heavy pages.
- Keep large dependency graphs (markdown parsers, ORM) on the server.
- Stream HTML so shells paint earlier (Core Web Vitals).
Pitfalls:
- Giant client islands reintroduced by one high
"use client"boundary. - Waterfalls of sequential awaits in one component — fetch in parallel (
Promise.all) when independent. - Serializing huge props to client children — pass ids and fetch on server instead.
- Importing server-only code into a client file (build errors or leaks).
Use server-only package to hard-fail accidental client imports of sensitive modules.
Auth and secrets
import 'server-only';
import { db } from './db';
export async function getPrivateUser(id: string) {
return db.user.findPrivate(id);
}
Env vars without NEXT_PUBLIC_ stay server-side. Don’t pass secrets as props to client components. Cookie/session reads belong in server code or middleware with a clear trust model.
Testing your mental model
Ask for every file:
- Does this need hooks or browser APIs?
- If yes, is it the smallest leaf that could own them?
- Does any prop I pass fail serialization?
- Am I duplicating data fetching on client after server already had it?
If you answer “I put use client on the page so it works,” push the boundary down.
Interview angle
Define RSC vs Client Component in Next. Explain default server, "use client" leaves, children composition, serializable props, and async server data fetching. Mention Server Actions as POST-like endpoints needing authz. Tie to JS weight and CWV.
Related on this site
- Next.js App Router overview
- Core Web Vitals
- LCP optimization tactics
- INP optimization tactics
- Browser rendering pipeline
- XSS
- Content Security Policy
Further reading
- Next.js: Server Components
- Next.js: Client Components
- Next.js: Server Actions
- React: Server Components
- React: Directives — use client
Related guides
- Next.js App Router Overviewapp/ directory mental model — layouts, server components by default, routing conventions, and how App Router differs from Pages Router.
- Auth Patterns OverviewAuth in Next.js App Router: cookies/sessions, middleware gates, server checks, and avoiding client-only security theater.
- Caching in Next.js OverviewNext.js caching layers: request memoization, data cache, full route cache, and router cache — what invalidates each.
- Client Components in Next.jsClient Components in the App Router: use client, hydration, bundling boundaries, and patterns that keep JS small.
- Data Fetching Patterns App RouterFetch data in Server Components with async/await and fetch caching: colocation, parallel requests, and client fallbacks.