Feb 15, 2026 · 2 min read
A practical mental model for React Server Components
How I think about server vs. client components in Next.js without the framework discourse.
The shortest version I tell teammates: server components are for fetching and shaping; client components are for interaction. Almost every confusion disappears once you accept that and stop trying to make one do the other's job.
The default is server
In the App Router, every component is a server component unless you mark it
with "use client". This means by default it:
- Runs on the server during render.
- Can
awaitdata — nouseEffect, no loading spinners for fetches. - Cannot use state, refs, or browser APIs.
- Is never sent to the browser. Its code stays on the server.
That last bullet is the one with the biggest payoff. If you've ever shipped a giant markdown library to the client just to render docs, server components fix that for free.
When to drop into a client component
Three triggers, and basically only these three:
- Interactivity —
onClick,onChange, focus management. - Local state —
useState,useReducer. - Browser APIs —
window,document,localStorage,IntersectionObserver.
If none of those apply, leave the component on the server.
Composition, not conversion
When you do need a client component, keep it small and lift the server work up:
// app/page.tsx — server component
export default async function Page() {
const posts = await getPosts();
return <PostList posts={posts} />;
}
// PostList.tsx — server component
import { LikeButton } from "./like-button";
export function PostList({ posts }) {
return posts.map((p) => (
<article key={p.id}>
<h2>{p.title}</h2>
<LikeButton postId={p.id} /> {/* tiny client island */}
</article>
));
}
// like-button.tsx
"use client";
export function LikeButton({ postId }) {
const [liked, setLiked] = useState(false);
// ...
}The data fetching, mapping, and structural markup stay on the server. Only the button — the smallest unit that needs state — is a client component.
Server actions are the missing half
Server components let you read state on the server. Server actions let you write it. Together they make a complete loop without an API route in sight.
// actions.ts
"use server";
export async function likePost(id: string) {
await db.post.update({ where: { id }, data: { likes: { increment: 1 } } });
revalidatePath(`/posts/${id}`);
}Call it from a form's action prop or directly from a client component.
The mental flip
If you're coming from Pages Router, the flip is: stop thinking "is this a page or a component?" and start thinking "is this a server boundary or a client island?" Pages are just one kind of server component. Loading states, error states, and layouts are server components too. Client components are leaves, not roots.
Once that clicks, the rest is mechanical.