React Server Components: The Mental Model That Finally Made It Click
React Server Components (RSCs) confused me for six months. I read the RFC, watched conference talks, built demo apps, and still could not shake the feeling that I was guessing when to use "use client" instead of understanding the underlying model.
Then the mental model clicked. And once it did, RSC decisions became obvious — not because I memorized rules, but because I understood the architecture.
The Wrong Mental Model
My initial (wrong) mental model: "Server Components run on the server, Client Components run on the client." This is technically true but completely unhelpful because it does not tell you why or when to choose one over the other.
With this model, every component became a question: "Does this need to be on the server or client?" And the answer was never obvious because most components could work in either context.
The Right Mental Model
The mental model that works: Server Components are the static shell. Client Components are the interactive islands.
Think of your page as a document. The document has structure: headings, paragraphs, images, navigation, and layout. This structure does not change based on user interaction. It is fetched once and rendered.
Embedded in this document are interactive widgets: forms that accept input, buttons that trigger actions, components that respond to mouse events, and elements that manage local state.
Server Components are the document structure. Client Components are the interactive widgets embedded within it.
The Practical Rule
Here is the decision process I use for every component:
Does this component need to: handle user events (onClick, onChange, onSubmit), use React hooks (useState, useEffect, useRef), access browser APIs (window, document, localStorage), or manage interactive state?
If yes to any: it must be a Client Component ("use client").
If no to all: it should be a Server Component (the default).
That is the entire decision framework. No complex analysis. No architecture debates. Just: does this component interact with the user or the browser?
Why Server Components Are the Default
In Next.js App Router, every component is a Server Component unless you explicitly opt into "use client." This is intentional and correct. Here is why:
Most components in a typical web application are presentational. They receive data (as props or from database queries) and render it. A blog post page renders markdown. A tool card displays a name, description, and rating. A navigation bar shows links. None of these require interactivity or local state.
Making these Server Components by default gives you: zero JavaScript shipped for static content (smaller bundle), direct database access (no API layer needed for reads), and automatic code-splitting (only interactive components are sent to the browser).
The "use client" Boundary
When you add "use client" to a file, that component and everything it imports become part of the client bundle. This is a module-level declaration, not a per-component declaration.
Critical insight: the boundary is at the import level. If a Client Component imports a utility function, that function is included in the client bundle. If a Server Component imports the same utility, it stays on the server.
This means: keep "use client" components as small and leaf-level as possible. Do not put "use client" on a layout component that wraps your entire page — that pulls everything into the client bundle.
Pattern: Server Component with Client Slots
The most common RSC pattern is a Server Component that fetches data and renders mostly static content, with small Client Components for interactive pieces:
A ToolPage Server Component: fetches the tool data from the database, renders the title, description, screenshots, and pricing information. This is all static HTML with no JavaScript.
Within it: a BookmarkButton Client Component (needs onClick), a RatingWidget Client Component (needs interactive stars), and a TabPanel Client Component (needs state for active tab).
The Server Component passes data as props to the Client Components. The Client Components add interactivity only where needed.
Pattern: Data Fetching in Server Components
One of the best RSC features: you can fetch data directly in the component without useEffect, without loading states, without an API route.
A Server Component can directly await a database query, an API call, or a file read. The component renders with the data already available. No loading state, no error boundary for fetch failures (you handle errors in the component directly), no waterfall of fetches.
This eliminates: the fetch-in-useEffect pattern (no more flashing loading spinners), the need for API routes that just proxy database queries, and the complexity of data-fetching libraries like React Query for server-rendered data.
When do you still need React Query or SWR? For client-side data that changes after the initial render. Real-time updates, user-triggered refreshes, optimistic updates — these still happen client-side and benefit from data-fetching libraries.
Pattern: Composition Over Prop-Drilling
How do you use a Client Component that needs server data? Through composition:
Instead of making the parent a Client Component so it can pass data to interactive children (which pulls everything into the client), keep the parent as a Server Component and pass data through props:
Server Component fetches data → renders Client Component with data passed as props → Client Component handles interactivity with the already-available data.
The key: Server Components can render Client Components and pass them serializable data (strings, numbers, objects, arrays). Client Components receive this data as props — no additional fetching needed.
What Cannot Cross the Boundary
The server-client boundary has serialization constraints. You can pass: strings, numbers, booleans, arrays, plain objects, Dates, Maps, Sets, and other serializable types.
You cannot pass: functions (they cannot be serialized across the network), class instances with methods, React elements with event handlers, or Symbols.
This means: if a Client Component needs a callback function, define it within the Client Component or pass it from another Client Component. Server Components cannot pass onClick handlers to Client Components directly.
Common Mistakes
Mistake 1: adding "use client" too high in the tree. If you add "use client" to a page layout because one button needs onClick, everything in that layout becomes a Client Component. Instead, extract the button into its own small Client Component file.
Mistake 2: trying to use hooks in Server Components. Server Components cannot use useState, useEffect, or any hook. If you need state, that component needs "use client."
Mistake 3: importing server-only code in Client Components. If a utility function uses Node.js APIs or accesses environment variables meant to be secret (database credentials), importing it in a Client Component exposes those to the browser. Use the "server-only" package to prevent accidental imports.
Mistake 4: over-fetching in Server Components. Just because you can directly query the database does not mean you should fetch everything. Fetch only what the component needs to render. Large data fetches still impact server response time.
Performance Reality
RSC performance benefits are real but nuanced:
For content-heavy pages (blog posts, documentation, product pages): RSCs dramatically reduce JavaScript bundle size. A blog post with 5000 words sends zero JavaScript for the content. Only interactive widgets (share button, newsletter signup) send JavaScript.
For highly interactive pages (dashboards, editors, spreadsheets): RSCs provide less benefit because most of the page is Client Components anyway. The shell might be server-rendered, but the interesting parts are all interactive.
For AIDToolStack: our tool pages are mostly Server Components (static content) with Client Component islands (bookmark button, rating widget, comparison checkbox). This gives us excellent performance — sub-50ms Time to Interactive on most pages.
The Mental Model Summary
Server Components = data + structure. No interactivity, no JavaScript, direct data access.
Client Components = interactivity + state. Event handlers, hooks, browser APIs.
The boundary = the "use client" directive. Everything below it (in the import tree) is client.
The pattern = Server Components that render small, focused Client Components as interactive islands within static content.
Once this mental model clicks, RSC decisions become automatic. You do not need to think about it — you just know which components need to be interactive and which are purely presentational.
Written by AIDToolStack Editorial
The AIDToolStack editorial team reviews developer tools, AI products, and MCP servers with hands-on testing and honest assessments. We use the tools we review every day in production.
References & Further Reading
Related Posts
Sponsor Our Newsletter
Reach thousands of developers who are actively evaluating AI tools, MCP servers, and dev infrastructure. Our weekly newsletter goes to engaged technical decision-makers.
All sponsored content is clearly labeled per our editorial policy.