WebMriya
Back to articles

React Server Components: Where the Boundary Belongs

A practical guide to React Server Components: what belongs on the server, what must stay interactive, and how to avoid accidental complexity.

React
Frontend Architecture
Software Architecture
W
Frontend Architecture & Legacy Modernization
October 2, 2026 · 6 min read

A product team adds a dashboard filter, a few authenticated queries, and one interactive table. Within weeks, the page ships a large JavaScript bundle, duplicated data-fetching logic, loading states that disagree, and components marked client-side simply because the boundary was unclear. The problem is not that React is too flexible. The problem is that server and browser responsibilities were never made explicit.

React Server Components (RSC) give a team another architectural boundary: some components render on the server and never become part of the browser bundle, while client components handle state, event handlers, effects, and browser APIs. The useful question is not whether Server Components are modern. It is which side of the boundary should own each piece of behavior.

The Boundary Is About Behavior, Not File Location

A Server Component can read data close to the source, call server-only modules, and render HTML or a React payload without sending its implementation to the browser. A Client Component can use useState, useEffect, event handlers, refs, and browser APIs, but its code must be delivered to the client.

Prefer a Server Component whenPrefer a Client Component when
The component primarily reads data and renders itThe component owns user interaction or local state
The code needs secrets, cookies, or server-only SDKsThe code needs window, document, WebSocket, or browser storage
The content benefits from smaller client bundlesThe UI must respond immediately without a server round trip
The result is mostly determined by URL or request dataThe component uses effects, refs, or event handlers

This is not a permanent division between entire pages and widgets. A server-rendered page can contain small client islands. A client component can receive server-rendered children. The boundary should follow interaction and data ownership, not the visual size of a component.

A Simple Composition Pattern

Consider an order page. The page needs customer-specific data, while the date filter needs browser interaction. Keep the data-loading shell on the server and isolate the filter as a client component.

tsx
import { DateFilter } from './DateFilter';
import { getOrders } from '@/server/orders';

export default async function OrdersPage({
  searchParams,
}: {
  searchParams: Promise<{ from?: string }>;
}) {
  const params = await searchParams;
  const orders = await getOrders({ from: params.from });

  return (
    <section>
      <DateFilter value={params.from ?? ''} />
      <OrdersTable orders={orders} />
    </section>
  );
}
tsx
'use client';

import { useRouter } from 'next/navigation';

export function DateFilter({ value }: { value: string }) {
  const router = useRouter();

  return (
    <input
      type="date"
      value={value}
      onChange={(event) => {
        const params = new URLSearchParams(window.location.search);
        params.set('from', event.target.value);
        router.push(`?${params.toString()}`);
      }}
    />
  );
}

The filter owns interaction, but the server owns the query. Changing the URL causes the page to render again with a new server-side result. This is a good fit when filtering does not need to update thousands of rows locally on every keystroke.

Do Not Pass Behavior Across the Boundary

Props crossing from a Server Component into a Client Component must be serializable. Plain objects, arrays, strings, numbers, booleans, and carefully chosen dates are usually safe. Functions, class instances, database connections, and server SDK objects are not.

This rule is architectural rather than cosmetic. If a client component needs a callback that closes over a database client, the design is asking the browser to own a server responsibility. Move the operation behind a server action or an API endpoint, then pass only the data and intent the client needs.

  • Pass identifiers and display data, not ORM models or database clients.
  • Keep authorization checks on the server even when the client hides an action.
  • Treat client input as untrusted when it returns to the server through a URL, form, or action.
  • Keep interactive state local unless multiple parts of the product genuinely need it.

The Most Common Mistakes

  1. Adding 'use client' to a high-level layout to fix one interactive button. This pushes every descendant toward the client boundary and often expands the bundle unnecessarily.
  2. Fetching the same resource in both server and client components. Decide whether the first render or client-side refresh owns the request; duplicating both usually creates race conditions.
  3. Using Server Components for interactions that require continuous local feedback. Dragging, rich text editing, canvas work, and rapid validation usually belong in a client component.
  4. Assuming Server Components automatically solve performance. They can reduce shipped JavaScript, but slow queries, oversized responses, poor caching, and expensive client dependencies still matter.
  5. Putting authorization only in the component tree. Rendering fewer buttons is not access control; the mutation or data access must enforce permissions at its real execution boundary.

A Practical Decision Process

Before choosing a boundary, trace the behavior rather than starting with the component tree.

  1. List the data the UI needs for its first meaningful render.
  2. Mark which data requires authentication, secrets, or server-only libraries.
  3. List every interaction: typing, selection, drag-and-drop, optimistic updates, timers, and browser APIs.
  4. Place the smallest possible client boundary around the interactions.
  5. Measure the result: client JavaScript, server response time, cache behavior, and interaction latency.
  6. Review the boundary when requirements change instead of preserving it as a rule forever.

The same discipline applies to larger React systems: explicit state ownership makes interfaces easier to extend, much like explicit UI states reduce the ambiguity of boolean flags. See React State Machines: Replace Boolean Flags With Explicit UI States for the related state-modeling problem.

When RSC Is the Wrong Tool

RSC is not a reason to move every component to the server. A design tool, collaborative editor, real-time monitoring surface, or highly dynamic form may need most of its logic in the browser. In those cases, forcing every update through a server render can increase latency and make the code harder to reason about.

It is also a poor fit when the framework integration is immature for the project’s deployment model, testing setup, or team experience. A boundary that is theoretically optimal but difficult to debug can cost more than a slightly larger bundle. Architecture should reduce total system complexity, not optimize one metric in isolation. The broader risks of unclear ownership are also covered in The Most Common Problems in Software Architecture.

A Useful Rule of Thumb

Start on the server when a component is primarily a data view. Move it to the client when it needs local interaction, effects, browser APIs, or immediate feedback. Keep that client boundary narrow, and make the server responsible for authorization, data access, and mutations.

For teams building a complex React product, the difficult part is rarely adding the directive itself. It is designing clear ownership across data, interaction, caching, authorization, and deployment. That is the kind of boundary work involved in complex web application development, especially when a product is growing faster than its original component structure.

Frequently asked questions

Do Server Components make Client Components obsolete?

No. Server Components are useful for data access and non-interactive rendering, while Client Components remain necessary for state, events, effects, and browser APIs. Most real applications need both.

Can a Client Component fetch data?

Yes, but that does not mean it should own every request. For initial server-rendered data, fetching on the server can reduce client JavaScript and centralize authorization; client fetching is appropriate for browser-driven refreshes and highly interactive views.

How small should a client boundary be?

As small as practical, but not artificially fragmented. Group behavior that shares state or interaction, then keep unrelated data views and server-only logic outside that boundary.