React Server Components trip people up because they quietly move a line developers spent a decade internalising. The fix is not more API memorisation — it is one clear mental model. Here is the one that makes RSCs stick.
Server by default, client on request
In the App Router every component is a Server Component unless you opt out with "use client". Server Components run only at build/request time on the server — they never ship to the browser, so they can read the filesystem, hit the database directly, and keep secrets. The moment you need interactivity (state, effects, event handlers, browser APIs) you mark that component "use client".
// app/page.tsx — a Server Component. No "use client".
// Runs on the server; its JS never reaches the browser.
import { db } from "@/lib/db";
import { LikeButton } from "./like-button";
export default async function Page() {
const post = await db.post.findFirst(); // direct DB access, no API route
return (
<article>
<h1>{post.title}</h1>
<LikeButton postId={post.id} /> {/* a client island */}
</article>
);
}"use client"; // this component (and its hooks) run in the browser
import { useState } from "react";
export function LikeButton({ postId }: { postId: string }) {
const [liked, setLiked] = useState(false);
return <button onClick={() => setLiked(!liked)}>{liked ? "♥" : "♡"}</button>;
}The boundary is a serialization boundary
The rule that resolves 90% of RSC confusion: props passed from a Server Component to a Client Component must be serializable. You can pass strings, numbers, plain objects, arrays — even JSX. You cannot pass a function, a class instance, or a Date-with-methods expectation across it.
This is why you can't pass an onClick handler from a Server Component into a client child — functions don't serialize. Define the handler inside the client component, and pass it plain data (the postId) instead.
Keep client islands small
The performance win of RSCs is shipping less JavaScript. That only happens if you push "use client" as far down the tree as possible — make the button a client component, not the whole page. A "use client" at the top of a route drags everything below it into the client bundle.
Hold those three ideas — server by default, a serialization boundary, small client islands — and the rest of RSCs is detail. You stop fighting the model and start getting the free data access and smaller bundles it was built to give you.