All guides
React — GuideReact

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.

Syed Wajeeh Haider
Syed Wajeeh Haider
Full Stack Software Engineer
Sep 28, 20263 min read
#React#Next.js#Server Components#Architecture

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.

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>
  );
}
No useEffect, no loading state, no API route: just await the data

'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.

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'
Interactivity lives at the edges; data and layout stay on the server

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.

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>
OrderList stays a Server Component even inside a client wrapper

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.

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.

Keep reading
(10)Let's talk

Ready to scaleyour application?

Stop worrying about technical debt. I can help you build a reliable, high-performance product from the ground up, tailored exactly to your business needs.

Actively open for new opportunities
Get in touch
Local time
--:-- PKT
© 2026 Syed Wajeeh Haider. All rights reserved.