Concept
The beginner framing: React Server Components (RSC) are components that run only on the server, they can talk directly to a database or file system, and they never ship their own code to the browser at all.
The precise mental model: every component in an RSC-enabled app is either a Server Component (the default) or a Client Component (opted into via a "use client" directive at the top of the file). The distinction isn't about where the HTML ends up, both kinds of components contribute to the same rendered page, it's about which environment the component's code runs in, and what gets shipped to the browser as JavaScript.
// ProductPage.jsx, a Server Component (the default, no directive needed)
async function ProductPage({ id }) {
const product = await db.products.findById(id); // direct database access, no API route needed
return (
<div>
<h1>{product.name}</h1>
<AddToCartButton productId={product.id} /> {/* a Client Component */}
</div>
);
}"use client"; // opts THIS file into being a Client Component
function AddToCartButton({ productId }) {
const [loading, setLoading] = useState(false); // hooks require a Client Component
return <button onClick={() => addToCart(productId)}>Add to cart</button>;
}ProductPage runs exclusively on the server: it can await a database call directly in its body (something no ordinary React component could ever do, since render must be synchronous, see Rendering Lifecycle), and none of its own code is ever sent to the browser. AddToCartButton needs useState and an onClick handler, both of which require running in the browser, so it's marked "use client", and its code (plus everything it imports) does get bundled and shipped.
What actually gets sent over the wire
A Server Component's output isn't HTML, and it isn't the JSON you'd get from a typical API, it's a special, React-specific serialized format (the "RSC payload") describing the resulting element tree, including placeholders for exactly where any Client Components need to be mounted with their props. The client-side React runtime reads this payload and reconstructs the tree, mounting Client Components as real, interactive React components at the marked positions.
The composition rule: client can't import server
"use client";
import ProductPage from "./ProductPage"; // ❌ build error, cannot import a Server Component from a Client ComponentA Client Component's module graph gets bundled for the browser, everything it statically imports has to be capable of running there too. A Server Component might contain server-only code (direct database calls, secrets, filesystem access) that must never reach the browser bundle, so the framework enforces this as a build-time error, not a runtime check. The correct direction is the opposite: a Server Component can render a Client Component directly, or pass one down as children/props from further up the tree, but a Client Component can never reach back up to import a Server Component's module.
// ✅ Server Component passing a Client Component as children, this works
function Layout({ children }) {
return <div className="page">{children}</div>; // Layout doesn't need to know what children IS
}
// A Server Component composes both together:
function Page() {
return (
<Layout>
<InteractiveWidget /> {/* Client Component, passed as children */}
</Layout>
);
}Try It
Predict what happens before checking the solution.
"use client";
import { formatCurrency } from "./utils/format"; // a plain utility, no directive
import ServerOnlyPricingLogic from "./ServerOnlyPricingLogic"; // a Server Component
function PriceTag({ amount }) {
return <span>{formatCurrency(amount)}</span>;
}