# React Server Components: A Practical Mental Model

> Server vs client components without the confusion: where the 'use client' boundary goes, what can cross it, and the mistakes that bloat your bundle.

- Author: Syed Wajeeh Haider
- Published: Sep 28, 2026
- Category: React
- Tags: React, Next.js, Server Components, Architecture
- Reading time: 3 min
- URL: https://syedwajeehhaider.com/react-server-components-mental-model

React Server Components are simple once the right picture clicks, and confusing until then. The picture is this: your component tree now spans two environments. Some components run only on the server and send rendered output; others ship to the browser as JavaScript. Almost every question comes down to which side a component is on.

- **2**: environments one tree can span: server and client
- **1**: directive that marks the boundary: 'use client'
- **0**: bytes of component code shipped for server-only components

## The default is the server

In the Next.js App Router, every component is a **Server Component** unless you say otherwise. Server Components can be `async`, can read the database or call internal APIs directly, and their code never reaches the browser. What they cannot do is use state, effects, event handlers or browser APIs, because none of that exists on the server.

*No useEffect, no loading state, no API route: just await the data*

```tsx
// app/orders/page.tsx: a Server Component
import { db } from '@/lib/db';

export default async function OrdersPage() {
  const orders = await db.order.findMany({ take: 20 });

  return (
    <ul>
      {orders.map((o) => (
        <li key={o.id}>{o.title}</li>
      ))}
    </ul>
  );
}
```

## 'use client' marks a boundary, not a component

Putting `'use client'` at the top of a file makes it an entry point into client code. Everything that file imports becomes part of the client bundle too. So the directive belongs on the **leaves** that need interactivity, such as a button, a form or a chart, not on a layout near the root.

*Interactivity lives at the edges; data and layout stay on the server*

```text
app/
  orders/
    page.tsx            server: fetches orders
    OrderList.tsx       server: renders rows
    StatusFilter.tsx    'use client': dropdown with state
    RefundButton.tsx    'use client': onClick + pending state
lib/
  db.ts                 import 'server-only'
```

## What can cross the boundary

When a Server Component renders a Client Component, its props are serialized and sent over the network. Plain data works: strings, numbers, arrays, plain objects, dates. Regular functions do not, because there is no way to send a closure to the browser. The exception is a **Server Action**, a function marked `'use server'` that React passes as a reference the client can call.

There is also a pattern that surprises people: a Client Component can render Server Components if they are passed in as `children` or other props. The server renders them first and the client component just places the result.

*OrderList stays a Server Component even inside a client wrapper*

```tsx
// Tabs.tsx
'use client';
export function Tabs({ children }: { children: React.ReactNode }) {
  const [open, setOpen] = useState(true);
  return (
    <div>
      <button onClick={() => setOpen(!open)}>Toggle</button>
      {open && children}
    </div>
  );
}

// page.tsx (server)
<Tabs>
  <OrderList />
</Tabs>
```

## Streaming with Suspense

Because Server Components can await data, a slow query could hold up the whole page. Wrap the slow part in `Suspense` with a fallback and the server streams the rest of the page immediately, filling in the slow section when it resolves. In Next.js, a `loading.tsx` file does the same for a whole route segment.

> **Guard your secrets:** Add `import 'server-only'` to modules that touch secrets or the database. If a Client Component ever imports one, even indirectly, the build fails instead of silently shipping your code to the browser.

## Common mistakes

- **'use client' in the root layout.** It turns the entire app into client code and throws away the benefits.
- **Context providers in Server Components.** Providers use React context, so they must be Client Components. Make a small client `Providers` wrapper and render `{children}` through it.
- **Reading `window` or `localStorage` in server code.** It does not exist there; move that logic into a Client Component or an effect.
- **Passing callbacks from server to client.** Pass data, or a Server Action, instead.
- **Fetching in a client effect what the server could fetch directly.** It adds a round trip and a loading spinner for no reason.

## Key takeaways

- Components are Server Components by default in the App Router.
- `'use client'` marks a boundary; everything imported below it ships to the browser.
- Only serializable props, or Server Actions, can cross from server to client.
- Pass Server Components as `children` to keep them on the server inside client wrappers.
- Use `Suspense` to stream slow parts and `server-only` to protect secrets.

Keep data and layout on the server, push interactivity to the leaves, and most of the confusion around Server Components disappears.
