Server Components change where code runs: less JavaScript shipped, direct data access, and a new boundary every modern React developer must understand.
React Server Components (RSC) are components that run only on the server (per request) or at build time. Their code is never sent to the browser. They can be async and read data directly:
export default async function Note({ id }) {
const note = await db.notes.get(id); // no API layer, no effect
return <article>{note.body}</article>;
}The output is serialised as an RSC payload and merged into the client's tree. Using them requires a bundler and framework integration; in practice that means a framework like Next.js App Router or React Router's RSC support. In such apps, components are Server Components by default.
Server Components can't use state, effects, event handlers or browser APIs. For interactivity you add Client Components: a module beginning with the "use client" directive marks a boundary; that module and everything it imports is bundled for the client (and still pre-rendered to HTML by SSR). Composition rules:
- A Server Component can render Client Components and pass them props that are serialisable: primitives, plain objects and arrays, Dates, Maps, Sets, promises, JSX, and Server Functions. Not class instances or ordinary functions.
- A Client Component can't import a Server Component, but it can receive one as
childrenor another JSX prop, so you can still nest server-rendered content inside client wrappers. - Push
"use client"down to the leaves (the button, not the page) to keep bundles small.
Server Components pair with Suspense: wrap a slow async component in <Suspense> and the rest streams first. You can also pass an unresolved promise to a Client Component and read it there with use().
Keep secrets server-side with the server-only package; any module accidentally imported into a client bundle then fails the build. "Server Components" are not the same as SSR: SSR turns a tree into HTML; RSC decides which components run where.
Example
// app/notes/[id]/page.jsx (Server Component - framework such as Next.js)
import { Suspense } from "react";
import { db } from "@/lib/db"; // server-only module
import LikeButton from "./LikeButton";
export default async function NotePage({ params }) {
const { id } = await params;
const note = await db.notes.find(id);
const commentsPromise = db.comments.forNote(id); // not awaited: streams later
return (
<article>
<h1>{note.title}</h1>
<p>{note.body}</p>
<LikeButton noteId={id} initialLikes={note.likes} />
<Suspense fallback={<p>Loading comments...</p>}>
<Comments promise={commentsPromise} />
</Suspense>
</article>
);
}
// LikeButton.jsx
// "use client";
// import { useState } from "react";
// export default function LikeButton({ noteId, initialLikes }) {
// const [likes, setLikes] = useState(initialLikes);
// return <button onClick={() => setLikes(l => l + 1)}>Like ({likes})</button>;
// }Remember
- Server Components run on the server/at build, can be async, and ship no JavaScript
"use client"marks a boundary; props crossing it must be serialisable- Pass Server Components into Client Components as children; keep client boundaries at the leaves
Watch out for
- Adding
"use client"at the top of a page or layout, turning the whole subtree into client code. - Passing a function or class instance from a Server Component to a Client Component prop.