Next.js Server Components
Render components on the server, fetch data directly, and ship less JavaScript to the browser.
TL;DR
- Treat every component in
appas a Server Component by default. - Fetch data with
awaitdirectly inside anasynccomponent. - Add
'use client'only to components that need interactivity.
Server Components by Default
async ComponentAwait data inside the component, with no effect or loading state needed.
export default async function Page() {
const posts = await getPosts();
return (
<ul>
{posts.map((p) => (
<li key={p.id}>{p.title}</li>
))}
</ul>
);
}Direct Data AccessQuery a database directly, since this code never reaches the browser.
import { sql } from '@/lib/db';
export default async function Users() {
const users = await sql`SELECT name FROM users`;
return <p>{users.length} users</p>;
}Private SecretsRead server-only environment variables without exposing them to the client.
export default async function Stats() {
const res = await fetch('https://api.example.com', {
headers: { 'x-api-key': process.env.API_KEY! },
});
return <p>{await res.text()}</p>;
}The Client Boundary
'use client'Mark a file as a Client Component so it can use state and events.
'use client';
import { useState } from 'react';
export function Like() {
const [on, setOn] = useState(false);
return (
<button onClick={() => setOn(!on)}>
{on ? 'Liked' : 'Like'}
</button>
);
}Server Renders ClientImport a Client Component into a Server Component and render it like any other.
import { Like } from './Like';
export default async function Post() {
const post = await getPost();
return (
<article>
<h1>{post.title}</h1>
<Like />
</article>
);
}Serializable PropsPass plain data across the boundary, such as strings, numbers, arrays, and objects.
// OK: plain data
<Chart points={[1, 2, 3]} title="Sales" />
// Error: plain functions cannot cross
<Chart onSelect={() => save()} />Composition Patterns
childrenPass Server Components through a Client Component by using the children prop.
// Panel is a Client Component
<Panel>
<ServerStats />
</Panel>Leaf ComponentsKeep interactive pieces small and low in the tree to ship less JavaScript.
export default async function Page() {
const product = await getProduct();
return (
<main>
<h1>{product.name}</h1>
<AddToCart id={product.id} />
</main>
);
}server-onlyFail the build if a server module is ever imported into client code.
// lib/data.ts
import 'server-only';
export async function getSecretData() {
return fetch(process.env.PRIVATE_URL!);
}Data Patterns
Promise.all()Start requests together to avoid a slow one-after-another waterfall.
const [user, orders] = await Promise.all([
getUser(id),
getOrders(id),
]);cache()Share one result across components during a single server render.
import { cache } from 'react';
export const getUser = cache(async (id: string) => {
return sql`SELECT * FROM users WHERE id = ${id}`;
});<Suspense>Stream a slow component in later while the rest of the page shows.
import { Suspense } from 'react';
<Suspense fallback={<p>Loading...</p>}>
<SlowReviews />
</Suspense>Tips
- Fetch data in the component that uses it, because repeated
fetchcalls for the same URL are deduplicated within one render. - Import
server-onlyin modules that touch secrets or the database, so a build error stops them from reaching the browser.
Warnings
- Server Components cannot use
useState,useEffect, or event handlers, so move that logic into a small Client Component. - Props passed to a
'use client'component must be serializable, so plain functions and class instances cause an error.
In Practice
Fetch a product on the server and add one small interactive button from the client.
- The page is a Server Component, so it fetches the product with await.
- Only LikeButton is a Client Component, so only its code ships to the browser.
- The page passes a plain number as a prop, which is serializable.
- The heading and description arrive as finished HTML with no JavaScript.
// app/products/[id]/page.tsx
import { LikeButton } from './LikeButton';
import { getProduct } from '@/lib/products';
export default async function Page(
props: PageProps<'/products/[id]'>
) {
const { id } = await props.params;
const product = await getProduct(id);
return (
<main>
<h1>{product.name}</h1>
<p>{product.description}</p>
<LikeButton initial={product.likes} />
</main>
);
}FAQ
A Server Component renders only on the server. Its code never ships to the browser, so it can read databases and secrets safely. Every component in the app folder is one unless its file starts with 'use client'.
Use one when you need state, effects, event handlers, or browser APIs like localStorage. Keep it small, and leave the data fetching in Server Components.
Yes. A Server Component can be an async function and await data before returning JSX. Client Components cannot be async.
No, they work together. Server Components decide where code runs. Client Components are still rendered to HTML on the server first, then made interactive in the browser.