A practical guide to server components, the use client directive, and rendering boundaries.
// app/page.tsx — runs entirely on the server, no client JS for this tree
export default async function Page() {
const post = await db.post.findFirst();
return <article>{post.title}</article>;
}
async functions and await data directly — there is no hydration step for their output.// Stays a Server Component: no interactivity, no hooks needed
export default async function PriceTag({ productId }) {
const price = await getPrice(productId);
return <span>${price}</span>;
}
useState, useEffect, event handlers, or browser-only APIs like localStorage.// Only LikeButton ships JS; the rest of the page stays server-rendered
import LikeButton from "./LikeButton"; // "use client" lives inside this file
export default async function Post({ id }) {
const post = await getPost(id);
return <article>{post.body}<LikeButton postId={id} /></article>;
}
children or another prop instead.// ClientShell.tsx ("use client") never imports ServerSidebar directly
export default function ClientShell({ children }) {
return <div className="shell">{children}</div>;
}
// app/page.tsx (Server Component) composes them together
<ClientShell><ServerSidebar /></ClientShell>
import a Server Component file directly; that import would pull server code into the client module graph and fail to build.// OK — every value below survives the server-to-client boundary
<ClientChart data={rows} title="Sales" updatedAt={new Date()} />
// Not OK — functions and class instances cannot cross as props
// <ClientChart onSort={sortRows} />
import "server-only";
// Any client-side import of this file now fails at build time, not runtime
export async function getSecretData() {
return db.query("SELECT * FROM secrets");
}
server-only package converts that silent leak into a build error, so the mistake is caught in CI instead of in production.client-only in browser-specific utilities so the two packages catch boundary mistakes in both directions.import { cacheLife, cacheTag } from "next/cache";
async function getPost(slug: string) {
"use cache"; // marks this function's result as cacheable
cacheTag(`post-${slug}`);
cacheLife("hours");
return db.post.findUnique({ where: { slug } });
}
use cache directive is the current recommended way to cache fetches, components, or whole routes, replacing the older unstable_cache API for new code.revalidateTag("post-my-slug") after a mutation, instead of waiting for cacheLife to expire.fetch calls still support the older { next: { revalidate: 3600 } } option directly, which works without enabling the use cache directive.const res = await fetch("https://api.example.com/data", { next: { revalidate: 3600 } });
<Suspense> so the rest of the page streams in while one section is still loading.<Suspense fallback={<p>Loading…</p>}>
<SlowServerSection />
</Suspense>
Yes — server components run in a Node.js environment and can call your database, ORM, or internal services directly using async/await. You only need an API route when the client itself needs to fetch data after the initial page load.
Add "use client" as the very first line of the file to make it a client component. Only components that actually need browser APIs, event listeners, or React hooks need this directive — keep it scoped to the smallest component possible.
Pass the data as props from the server component down to the client component — but only serialize-safe values like strings, numbers, and plain objects. Functions, class instances, and Promises cannot cross the server-client boundary as props.
Any module imported by a client component (directly or transitively) gets bundled for the browser. Use the server-only package in files that must stay server-side — it throws a build-time error if that module is ever pulled into the client graph.
You can't import a server component inside a client component, but you can pass one as children or a prop — the server component still renders on the server, and only its output HTML is sent to the client component as a slot. This is the key pattern for keeping heavy server logic out of the client bundle.