Server Components
React Server Components (RSC) run only on the server: their code never
reaches the browser, they can be async and read a database or secrets
directly. In the App Router every component is a Server Component unless it is
under a 'use client' boundary. The server renders them into the RSC
Payload, a serialized tree that also holds references to Client Components;
Next.js then turns it into HTML. RSC is not the same as SSR: SSR is about
producing HTML, RSC is about where a component runs.
Applies to: Next.js 16.4, React 19.3.
Details
RSC vs SSR
| Server Components (RSC) | SSR | |
|---|---|---|
| What it is | A kind of component | A way to produce the initial HTML |
| JavaScript in the browser | None for these components | The code of all components, for hydration |
| Output | RSC Payload (and HTML from it) | HTML |
| On client navigation | Rendered again on the server; payload is sent | Not involved: the client renders |
| Hydration | Not hydrated | Hydrated |
Client Components are rendered to HTML on the server too (this is SSR) and then hydrated. A Pages Router app has SSR but no RSC.
What Server Components Cannot Do
- Use state, effects or other client hooks (
useState,useEffect,useReducer), and context (useContext). - Have event handlers (
onClick,onChange). - Use browser APIs (
window,localStorage). - Pass functions to Client Components as props, except Server Actions: props must be serializable.
For any of that, move the part into a Client Component.
How a Route Is Rendered
On the server:
- React renders Server Components into the RSC Payload.
- Next.js uses the payload and the Client Component code to render HTML.
In the browser:
- The HTML is shown immediately, not yet interactive.
- The RSC Payload is used to reconcile the Server and Client Component trees.
- Client Components are hydrated and become interactive.
Hydration
- Hydration attaches event handlers and state to the server HTML; the first client render must match it. Causes of a mismatch and the fixes are in React in the Browser.
- In Next.js a browser-only part can also be rendered with
next/dynamicandssr: falseinside a Client Component.
Rendering Strategies
- Static: HTML is made at build time or on revalidation, cached and served to everyone.
- Dynamic: HTML is made on every request, because the output depends on the
request:
cookies(),headers(),searchParams,connection(). - Streaming: the response is sent in parts; each
<Suspense>boundary shows its fallback first and is replaced when its content is ready.loading.tsxwraps the page of its segment in such a boundary.
How they combine depends on the caching model:
- Cache Components
- Previous model
- The choice is made per component, not per route (Partial
Prerendering). At build time the static markup, the
'use cache'parts and the<Suspense>fallbacks form a prerendered static shell; the parts inside<Suspense>that read request data or uncached data render on request and stream in. - Uncached data or request data outside
<Suspense>fails the build. - See Caching.
- The choice is made per route: one
cookies(),headers(),searchParams,connection()orcache: 'no-store'makes the whole route dynamic. - A route without them is static, even if it fetches uncached data: the data is fetched at build time.
- Streaming is visible only in a dynamic route; a static one is sent complete.
Example
Streaming: the heading arrives at once, each list replaces its fallback when its
data is ready. This is how it works with Cache Components. In the previous model
the page uses no request data and is prerendered whole at build time; adding
await connection() at the top of Posts and Users makes them render on
request.
- app/page.tsx
- app/posts.tsx
- app/users.tsx
import { Suspense } from 'react';
import { Posts } from './posts';
import { Users } from './users';
export default function Page() {
return (
<main>
<h1>Dashboard</h1>
<Suspense fallback={<p>Loading posts...</p>}>
<Posts />
</Suspense>
<Suspense fallback={<p>Loading users...</p>}>
<Users />
</Suspense>
</main>
);
}
type Post = { id: number; title: string };
export async function Posts() {
const res = await fetch('https://jsonplaceholder.typicode.com/posts');
const posts: Post[] = await res.json();
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
import { db } from '@/lib/db';
export async function Users() {
const users = await db.user.findMany(); // direct database access
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
Follow-up Questions
- Can a Client Component import a Server Component? No: everything a client
module imports becomes client code. Pass the Server Component as
childrenor another prop instead. - Are Server Components the same as SSR? No, see the table above. SSR exists without RSC, and RSC also works on client navigation without producing HTML.
- What does streaming improve? The user sees the page shell and the fast parts sooner instead of waiting for the slowest query.
- How do you keep server-only code out of the client? Import
server-onlyin the module: importing it from a Client Component becomes a build error.
Common Mistakes
suppressHydrationWarningon a parent element to silence a mismatch: it works only one level deep and hides the bug; move the browser-only read intouseEffect.- "Server Components replace SSR" or "Client Components render only in the browser": Client Components are prerendered on the server as well.
- Adding
'use client'to a whole page because of one button: the boundary should be as low in the tree as possible. - Making a whole layout a Client Component to add a provider: wrap only
{children}in a small provider component, see Client Components.