Skip to content
All notes

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.

react
nextjs
frontend

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 await data — no useEffect, 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:

  1. InteractivityonClick, onChange, focus management.
  2. Local stateuseState, useReducer.
  3. Browser APIswindow, 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.