React Server Components Boundary
Learn React Client-Server Communication - Part 039
React Server Components boundary, server/client module graph, serialization constraints, data ownership, streaming payloads, security, and production design rules for React client-server communication.
Part 039 — React Server Components Boundary
Target mental model: React Server Components move part of your component tree into a server execution environment. They do not erase the client-server boundary; they make the boundary more explicit, more powerful, and easier to misuse.
Part 038 closed Phase 5 by treating navigation performance as a graph scheduling problem.
Phase 6 starts with a bigger shift: React can now express UI whose component execution happens on the server and whose result is consumed by the client as a serialized React payload.
That sounds simple.
It is not.
The failure mode is to think:
Server Component = normal component, but faster
A better model:
Server Component = server-side computation that returns a serializable UI description.
Client Component = browser-side computation that owns interactivity and browser effects.
The boundary between them is a protocol boundary.
Once you see the boundary as a protocol, many rules become obvious:
- server code can read databases and secrets;
- client code can use browser APIs and interactive hooks;
- data crossing the boundary must be serializable;
- module references crossing the boundary must be understood by the bundler/framework;
- authorization must still happen on the server;
- client interaction still requires client JavaScript;
- streaming changes timing, not ownership;
- server execution does not automatically make APIs safe.
This part is about the boundary.
Not Next.js routing.
Not framework-specific file conventions.
The goal is to build the runtime model you need to design React client-server communication without cargo-culting "use client" and "use server".
1. The Three Different Things People Confuse
When people say “server-side React”, they often mix three different concepts.
1.1 Server-Side Rendering
SSR renders React to HTML on the server.
React tree --> HTML string/stream --> browser DOM --> hydrate client app
SSR is about initial HTML generation.
The browser still receives client JavaScript for the interactive tree.
1.2 React Server Components
React Server Components execute selected components in a server environment.
Server Component execution --> serialized React payload --> client reconciles tree
RSC is about where component code runs and what gets serialized to the client.
A Server Component is not shipped to the browser as component code.
1.3 Server Functions / Actions
Server Functions are functions that run on the server but can be referenced from client code.
client calls function reference --> network request --> server executes function --> serialized result
Server Functions are about client-triggered server execution.
They are not Server Components.
They often appear together, but they are different primitives.
The most important distinction:
| Concept | Runs on server? | Sends component code to browser? | Main purpose |
|---|---|---|---|
| SSR | Yes | Usually yes for hydration | Fast initial HTML |
| Server Component | Yes | No for that component code | Server-owned UI/data composition |
| Client Component | No | Yes | Browser interactivity |
| Server Function | Yes | Function implementation no, reference yes | Server-side action callable from client |
2. The Runtime Topology
A modern RSC-capable React app has at least two execution environments.
The server environment can do things the browser must not:
- access private databases;
- read secrets;
- call internal services;
- perform privileged authorization checks;
- use server-only libraries;
- keep implementation details out of the client bundle.
The browser environment can do things the server cannot:
- respond to clicks immediately;
- use
useState,useReducer, and event handlers; - access DOM and layout;
- use browser-only APIs;
- maintain per-tab ephemeral state;
- subscribe to browser lifecycle events.
The boundary is not a stylistic preference.
It is a hard runtime separation.
3. Server Components Are Server-Executed UI Builders
A Server Component is best understood as an async server-side UI builder.
// Server Component
export default async function InvoicePage({ invoiceId }: { invoiceId: string }) {
const invoice = await invoiceRepository.getById(invoiceId);
return (
<main>
<h1>Invoice {invoice.number}</h1>
<InvoiceSummary invoice={invoice} />
</main>
);
}
The important thing is not the syntax.
The important thing is the execution location.
The component can await server data because it runs before the client receives the result.
But the component cannot own browser interactivity:
// Wrong as a Server Component
export default function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
Interactive behavior belongs in a Client Component.
'use client';
import { useState } from 'react';
export function CounterButton() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
Then the Server Component can compose the Client Component.
import { CounterButton } from './CounterButton';
export default async function Page() {
const initialCount = await getInitialCount();
return (
<section>
<h1>Dashboard</h1>
<CounterButton initialCount={initialCount} />
</section>
);
}
The Server Component builds the server-owned part.
The Client Component owns the interactive part.
4. "use client" Marks a Client Module Boundary
The "use client" directive is not a runtime call.
It is a source directive consumed by the framework/bundler.
'use client';
import { useState } from 'react';
import { formatCurrency } from './formatCurrency';
export function PriceEditor() {
const [value, setValue] = useState('');
return <input value={value} onChange={(e) => setValue(e.target.value)} />;
}
Once a module is marked with "use client", that module and its transitive dependencies become part of the client module graph.
That means this import can be dangerous:
'use client';
import { calculatePrice } from '../domain/pricing';
If calculatePrice imports server-only code, secrets, Node APIs, or heavy dependencies, you have a boundary violation.
The rule:
A client module must only import code that is safe and intended to execute in the browser.
4.1 "use client" Is Viral Downward
If PriceEditor.client.tsx imports formatCurrency.ts, formatCurrency.ts is client code for that import path.
A module can sometimes be evaluated in different environments depending on who imports it, but once it is inside the client graph, it must be browser-safe.
4.2 Prefer Small Client Islands
Bad boundary:
'use client';
export default function WholePage() {
// everything below ships to the client
}
Better boundary:
// Server Component
export default async function ProductPage({ id }: { id: string }) {
const product = await getProduct(id);
return (
<main>
<ProductReadModel product={product} />
<AddToCartButton productId={product.id} />
</main>
);
}
Only AddToCartButton needs browser interactivity.
Keep the rest server-owned.
5. There Is No "use server" for Server Components
This is a common misunderstanding.
'use server'; // not how you mark a Server Component
export default function Page() {
return <h1>Hello</h1>;
}
Server Components are server by default in RSC-capable frameworks unless they are inside a client module graph.
"use server" marks Server Functions, not Server Components.
This distinction matters because the security model is different.
A Server Component is executed as part of rendering.
A Server Function is callable from the client through a network request.
That means a Server Function is effectively an endpoint.
Part 040 covers that in depth.
6. What Crosses the RSC Boundary
In classic client-side React, component data crosses the network through JSON APIs.
In RSC, UI description and serialized values cross through the React Server Component payload.
Conceptually:
Server Component execution output
-> serialized component tree description
-> references to Client Component modules
-> serializable props for Client Components
-> streamed chunks when available
Do not rely on the exact wire format for application logic.
Treat it as React/framework protocol.
What matters for architecture is the boundary contract:
| Thing | Can cross? | Notes |
|---|---|---|
| string/number/boolean/null | Yes | Serializable primitives |
| plain object/array of serializable values | Yes | Must remain data, not behavior |
| Date-like values | Framework-dependent serialization rules | Normalize deliberately |
| function implementation | No | Except Server Function reference mechanism |
| class instance with methods | No / avoid | Loses behavior or fails serialization |
| database connection | No | Server-only resource |
| secret | Must not | Boundary leak |
| event handler | No from Server to Client | Event handlers require Client Components |
| Client Component reference | Yes through bundler protocol | Client module reference, not code execution on server |
The practical rule:
Pass DTOs across the Server Component -> Client Component boundary.
Do not pass domain objects with behavior.
7. Boundary Serialization Is a Design Constraint
Assume you have a rich domain object:
class Invoice {
constructor(
public id: string,
public totalCents: number,
public status: 'draft' | 'issued' | 'paid',
) {}
canBeCancelled() {
return this.status === 'draft' || this.status === 'issued';
}
}
Do not pass that class instance into a Client Component.
Create an explicit view model.
export type InvoiceView = {
id: string;
totalCents: number;
status: 'draft' | 'issued' | 'paid';
canCancel: boolean;
};
export function toInvoiceView(invoice: Invoice): InvoiceView {
return {
id: invoice.id,
totalCents: invoice.totalCents,
status: invoice.status,
canCancel: invoice.canBeCancelled(),
};
}
Then pass only the view model.
export default async function InvoicePage({ id }: { id: string }) {
const invoice = await invoiceRepository.getById(id);
return <InvoiceClientPanel invoice={toInvoiceView(invoice)} />;
}
This is not boilerplate.
It is boundary hygiene.
A view model gives you:
- serialization safety;
- stable client contract;
- permission-filtered data;
- no accidental method leakage;
- testable boundary output;
- compatibility with streaming and caching.
8. Server Components Do Not Remove API Design
A dangerous conclusion:
If Server Components can query the database, we no longer need API boundaries.
That is wrong.
You may not need a public JSON endpoint for the same read model, but you still need a boundary contract.
The contract just moved from:
GET /api/invoices/:id -> JSON
to:
Server Component -> Client Component props / rendered output
You still need to decide:
- who owns authorization;
- what fields are allowed to leave the server;
- what shape the client receives;
- how loading and errors are represented;
- how stale data is handled;
- how mutations revalidate the view;
- how observability correlates render work with user experience.
The server-client boundary remains.
Only the mechanism changes.
9. Data Ownership in Server Components
Server Components are strongest when the server owns the read model.
Good candidates:
- product pages;
- dashboards with server-assembled read models;
- documentation pages;
- CMS content;
- account pages with permission-filtered data;
- reports where data is expensive to compute and not highly interactive;
- route shells that prepare data for client islands.
Poor candidates:
- highly interactive editors;
- local draft-heavy workflows;
- drag-and-drop boards requiring immediate local interaction;
- live collaborative surfaces with frequent client-originated updates;
- canvas-heavy UI;
- stateful widgets dominated by browser APIs.
A practical split:
Server Component:
- assemble initial read model
- enforce authz
- reduce overfetching
- keep heavy dependencies server-side
- render non-interactive structure
Client Component:
- own events
- own local draft state
- own browser API integration
- own optimistic interaction
- subscribe to realtime/browser lifecycle
10. Example: Server-Owned Read Model + Client-Owned Command Surface
// app/invoices/[id]/page.tsx
import { InvoiceCommandPanel } from './InvoiceCommandPanel';
import { getInvoicePage } from '@/server/invoices/getInvoicePage';
export default async function InvoicePage({ params }: { params: { id: string } }) {
const page = await getInvoicePage(params.id);
return (
<main>
<header>
<h1>Invoice {page.invoice.number}</h1>
<p>{page.invoice.statusLabel}</p>
</header>
<section>
<h2>Lines</h2>
<ul>
{page.lines.map((line) => (
<li key={line.id}>{line.description} — {line.amountLabel}</li>
))}
</ul>
</section>
<InvoiceCommandPanel
invoiceId={page.invoice.id}
allowedCommands={page.allowedCommands}
/>
</main>
);
}
// app/invoices/[id]/InvoiceCommandPanel.tsx
'use client';
export function InvoiceCommandPanel({
invoiceId,
allowedCommands,
}: {
invoiceId: string;
allowedCommands: Array<'issue' | 'cancel' | 'pay'>;
}) {
return (
<div>
{allowedCommands.includes('issue') && <button>Issue</button>}
{allowedCommands.includes('cancel') && <button>Cancel</button>}
{allowedCommands.includes('pay') && <button>Pay</button>}
</div>
);
}
Notice the boundary.
The client receives:
- an invoice ID;
- a list of allowed command names;
- no raw permission graph;
- no database object;
- no server secret;
- no implementation detail.
The server still must authorize every command when it is executed.
The UI permission is convenience, not security.
11. Server Components and Authorization
Server Components can improve authorization hygiene because data fetching can happen in server-only code.
But they can also hide authorization mistakes.
Bad:
export default async function AdminPanel() {
const users = await db.user.findMany();
return <UserTable users={users} />;
}
Better:
export default async function AdminPanel({ viewer }: { viewer: Viewer }) {
await requirePermission(viewer, 'user.read.all');
const users = await userReadModel.listForAdmin(viewer.tenantId);
return <UserTable users={users.map(toUserRow)} />;
}
Rules:
- authorize before reading sensitive data;
- scope every query by tenant/account/org;
- map data to permission-filtered DTOs;
- never rely on hidden client code for security;
- treat Client Component props as exposed to the browser;
- log denied render attempts as security-relevant events.
Server Components are not a permission system.
They are a safer place to enforce one.
12. Server Components and Secrets
A server-only module may read secrets.
// server/payments.ts
export async function getPaymentConfig() {
return {
providerAccountId: process.env.PAYMENT_ACCOUNT_ID!,
secretKey: process.env.PAYMENT_SECRET_KEY!,
};
}
But the return value from a Server Component can still leak secrets if you pass it down.
Bad:
const config = await getPaymentConfig();
return <PaymentWidget config={config} />;
Better:
const publishableConfig = await getPublishablePaymentConfig();
return <PaymentWidget accountId={publishableConfig.accountId} />;
Boundary rule:
Server-only access does not mean server-only output.
Every value passed to a Client Component must be treated as public to that browser session.
13. Boundary Placement Heuristics
A boundary should be placed where ownership changes.
Not where file organization feels convenient.
13.1 Put the Boundary Low When Only a Small Piece Is Interactive
export default async function ArticlePage() {
const article = await getArticle();
return (
<article>
<MarkdownBody source={article.body} />
<LikeButton articleId={article.id} />
</article>
);
}
Only LikeButton needs client execution.
13.2 Put the Boundary Higher When Interaction Owns the Whole Surface
export default async function EditorPage({ id }: { id: string }) {
const initialDraft = await getDraft(id);
return <RichEditor initialDraft={initialDraft} />;
}
A rich editor may own browser events, selection, keyboard shortcuts, autosave, undo stack, and local draft state.
Forcing tiny client islands inside it can make design worse.
13.3 Put Server Work Behind the Boundary When It Requires Privilege
export default async function BillingPage() {
const billing = await getBillingReadModelForViewer();
return <BillingSummary billing={billing} />;
}
The read model belongs server-side because it needs authorization, sensitive filtering, and internal service calls.
14. Boundary Decision Matrix
| Requirement | Prefer Server Component | Prefer Client Component |
|---|---|---|
| Reads database directly | Yes | No |
| Needs secret/internal token | Yes | No |
Uses useState/event handlers | No | Yes |
| Uses DOM measurements | No | Yes |
| Large server-only dependency | Yes | No |
| Highly interactive local draft | Usually no | Yes |
| SEO/static content | Often yes | Sometimes |
| User-specific permission-filtered read model | Often yes | Client receives filtered output |
| Realtime subscription | Initial render yes | Subscription client-side |
| Needs browser storage | No | Yes |
The more a component is about read model assembly, the more it belongs server-side.
The more a component is about interaction lifecycle, the more it belongs client-side.
15. Data Fetching in Server Components
Server Components let you colocate server data reads with server-rendered UI.
export default async function CustomerSummary({ customerId }: { customerId: string }) {
const customer = await customerRepository.getSummary(customerId);
return (
<section>
<h2>{customer.name}</h2>
<p>{customer.segment}</p>
</section>
);
}
This reduces client waterfalls because the browser does not need to first download JavaScript, render, then discover the fetch.
But it can create server waterfalls.
Bad:
async function OrderList({ customerId }: { customerId: string }) {
const orders = await getOrders(customerId);
return (
<ul>
{orders.map((order) => (
<OrderRow key={order.id} orderId={order.id} />
))}
</ul>
);
}
async function OrderRow({ orderId }: { orderId: string }) {
const order = await getOrder(orderId);
return <li>{order.number}</li>;
}
This can become server-side N+1.
Better:
async function OrderList({ customerId }: { customerId: string }) {
const orders = await getOrderRowsForCustomer(customerId);
return (
<ul>
{orders.map((order) => (
<li key={order.id}>{order.number}</li>
))}
</ul>
);
}
RSC does not remove data modeling.
It moves part of the modeling to the server render graph.
16. Server Render Graph vs Client Fetch Graph
Before RSC:
With RSC:
The client waterfall can shrink.
But the server graph becomes more important.
You must still ask:
- are server fetches parallelized;
- are repeated reads deduped;
- are per-user caches scoped safely;
- are errors isolated by boundary;
- are slow subtrees streamed;
- are expensive reads moved out of hot render paths;
- are permission checks done before data access.
17. Streaming Changes Timing, Not Semantics
RSC-capable frameworks can stream parts of the response.
Streaming is useful when:
- shell can render before slow data;
- above-the-fold content is fast;
- below-the-fold content can arrive later;
- independent subtrees have different latency;
- fallback UI is meaningful.
Streaming does not change who owns data.
It changes when bytes arrive.
Bad streaming:
stream random skeletons without product meaning
Good streaming:
stream stable layout and critical decision context first;
stream slow, independent, non-blocking sections later
18. Error Boundaries and RSC
A Server Component can fail during server render.
Failure can happen because:
- database is unavailable;
- authorization denies access;
- input parameter is invalid;
- internal service times out;
- serialization fails;
- a child server subtree throws.
Design errors as boundaries.
export default async function Page({ params }: { params: { id: string } }) {
const invoice = await getInvoiceOrThrow(params.id);
return <InvoiceView invoice={invoice} />;
}
Then isolate rendering failures at route/subtree level.
critical page shell
-> invoice summary boundary
-> activity feed boundary
-> recommendations boundary
A dashboard should not become blank because an optional recommendation widget failed.
Part 043 goes deeper into streaming and partial data.
The key here:
Server execution introduces server failure into the render path.
Treat render failure like API failure.
19. Client Component Props Are an API
This code is a contract:
<ProductActions
productId={product.id}
canBuy={permissions.canBuy}
stockLabel={product.stockLabel}
/>
It deserves the same care as an API response.
Ask:
- is
productIdstable; - can
canBuybecome stale; - is
stockLabellocale-specific; - should the client know raw stock count;
- what happens if user clicks after permission changes;
- how does mutation refresh the server-rendered view;
- can this prop leak tenant data;
- does the prop shape force client rework when backend changes.
A good Client Component prop contract is narrow.
Bad:
<ProductActions product={fullProductFromDatabase} viewer={fullViewerObject} />
Better:
<ProductActions
productId={product.id}
commands={['add-to-cart', 'save']}
displayPrice={product.displayPrice}
/>
20. Server Components and Client Cache
Do Server Components replace React Query, SWR, Apollo, or a client cache?
Not universally.
They solve different problems.
RSC is strong for:
- initial server-owned read model;
- route-level composition;
- reducing client bundle/data waterfalls;
- server-only data access;
- streaming UI from server.
Client cache remains useful for:
- highly interactive refetching;
- background freshness;
- optimistic updates;
- infinite scrolling;
- polling/realtime integration;
- tab-local state preservation;
- client-only workflows after initial render.
A common hybrid:
Server Component:
render initial route read model
Client Component:
hydrate or seed query cache for interactive sections
own mutations and optimistic state
invalidate/refetch after commands
Avoid duplicate ownership.
Bad:
same invoice read model owned by RSC output, React Query cache, Zustand store, and local state
Better:
RSC owns initial page shell;
query cache owns interactive activity feed refresh;
local state owns form draft.
21. Mutations From a Server Component World
A Server Component cannot handle a click directly.
Clicks happen in the browser.
The common mutation paths are:
- Client Component calls JSON/RPC API;
- Client Component submits a form/action;
- Client Component calls a Server Function reference;
- route action handles mutation and revalidates route data;
- realtime event updates client after server mutation.
The important point:
Server Components are read/render primitives.
Mutations still need an action boundary.
That action boundary needs:
- validation;
- authorization;
- idempotency when necessary;
- conflict detection;
- audit logging;
- revalidation;
- client pending/error state.
Part 040 handles Server Functions and Actions as that boundary.
22. Naming Conventions That Prevent Boundary Bugs
Use file/module naming to make boundary visible.
Examples:
InvoicePage.server.tsx
InvoiceCommandPanel.client.tsx
invoice.read-model.server.ts
invoice.actions.server.ts
invoice.dto.ts
formatCurrency.shared.ts
The suffixes are not universal React requirements.
They are design aids.
The goal is to prevent accidental imports.
A simple convention:
| Suffix | Meaning |
|---|---|
.server.ts | Must never enter browser bundle |
.client.tsx | Browser-safe, may use hooks/events/DOM |
.shared.ts | Safe in both runtimes |
.dto.ts | Serializable boundary types |
.schema.ts | Runtime validation schema |
Then enforce with lint/build rules where possible.
23. The Shared Module Trap
A “shared” module often becomes a dumping ground.
Bad:
// shared/domain.ts
import { db } from '../server/db';
export function formatInvoice(invoice: Invoice) {
return `${invoice.number}`;
}
export async function getInvoice(id: string) {
return db.invoice.findUnique({ where: { id } });
}
If a Client Component imports formatInvoice, bundling may discover server-only imports.
Better:
invoice.format.shared.ts // pure formatting only
invoice.repository.server.ts // database only
invoice.dto.ts // serializable shapes
invoice.policy.server.ts // permission checks
Shared code must be boring.
Pure functions.
No secret reads.
No filesystem.
No database.
No Node-only APIs.
No hidden global mutable state.
24. RSC and Bundle Size
A Server Component's implementation does not need to ship to the client as browser-executed component code.
This can reduce bundle size when you keep heavy code server-side.
Good:
// Server Component
import { expensiveMarkdownRenderer } from '@/server/markdown';
export default async function ArticleBody({ id }: { id: string }) {
const article = await getArticle(id);
const html = await expensiveMarkdownRenderer(article.markdown);
return <div dangerouslySetInnerHTML={{ __html: html }} />;
}
Bad:
'use client';
import { expensiveMarkdownRenderer } from '@/server/markdown';
The second version likely pulls expensive or invalid server dependencies toward the browser.
Boundary placement directly affects bundle size.
But do not optimize bundle size at the cost of broken interaction ownership.
A rich editor needs client code.
A rendered article body usually does not.
25. RSC and Backend Load
RSC can reduce browser requests.
It can also increase server render work.
Watch for:
- repeated DB reads per component;
- expensive render-time computations;
- low cache hit rate;
- per-request duplication;
- N+1 service calls;
- slow authorization checks repeated in child components;
- high cardinality per-user render cache;
- streaming many tiny chunks with overhead;
- server actions causing broad revalidation storms.
A strong RSC architecture treats server render as a backend workload.
It needs:
- deadlines;
- batching;
- caching;
- tracing;
- circuit breakers for optional panels;
- query planning;
- explicit dependency graphs;
- load tests.
React did not make backend performance disappear.
It moved more frontend-owned reads into backend render paths.
26. Observability for RSC Boundary
You need traces that show:
navigation request
-> server render
-> authz check
-> read model fetches
-> component subtree render
-> stream chunks
-> client hydration/reconciliation
-> client island interaction
At minimum, capture:
- route ID;
- viewer/tenant scope without leaking PII;
- render duration;
- slow server data dependency;
- number of server data calls;
- streamed chunk timing;
- serialization failures;
- client hydration errors;
- client island bundle size;
- mutation-to-revalidation latency.
Boundary bugs are hard to debug if traces stop at “HTTP 200”.
An RSC response can be HTTP-successful but UI-broken.
27. Testing Server/Client Boundaries
Test the boundary at multiple levels.
27.1 Server Read Model Test
it('returns only fields allowed for invoice viewer', async () => {
const viewer = viewerWithPermission('invoice.read');
const page = await getInvoicePage({ viewer, invoiceId: 'inv_123' });
expect(page.invoice).toEqual({
id: 'inv_123',
number: 'INV-123',
statusLabel: 'Issued',
});
expect(page).not.toHaveProperty('internalRiskScore');
});
27.2 Serialization Test
import { expectSerializable } from './test/serialization';
it('invoice page model is serializable', async () => {
const model = await getInvoicePageModel(testInput);
expectSerializable(model);
});
27.3 Client Island Test
it('disables unavailable commands', () => {
render(<InvoiceCommandPanel invoiceId="inv_1" allowedCommands={['pay']} />);
expect(screen.getByRole('button', { name: /pay/i })).toBeEnabled();
expect(screen.queryByRole('button', { name: /cancel/i })).not.toBeInTheDocument();
});
27.4 Import Boundary Test
Use lint/build rules to prevent .client.tsx from importing .server.ts.
client module import graph must not include server-only module
This is not optional in large teams.
One bad import can move secret-bearing or Node-only code toward the wrong runtime.
28. Failure Mode Catalog
28.1 Accidental Whole-Page Clientification
Symptom:
large bundle, lost server-only benefits, more client waterfalls
Cause:
"use client" placed too high
Fix:
move interactivity into smaller client islands
28.2 Server-Only Import in Client Graph
Symptom:
build failure, runtime failure, secret exposure risk
Cause:
client module imports shared module that imports server dependency
Fix:
split shared/server modules; enforce lint rules
28.3 Non-Serializable Prop
Symptom:
serialization error, hydration mismatch, missing behavior
Cause:
passing class instance/function/db object to Client Component
Fix:
map to explicit DTO/view model
28.4 Hidden Authorization Leak
Symptom:
client receives fields it should not know
Cause:
server read model returns raw entity
Fix:
authorize, scope, project, redact
28.5 Server Render N+1
Symptom:
route is slow under load despite fewer browser requests
Cause:
nested Server Components each fetch independently
Fix:
batch, preload, aggregate, cache per request
28.6 Duplicate State Ownership
Symptom:
RSC output says one thing, client cache says another
Cause:
same read model owned by multiple layers
Fix:
assign ownership: RSC initial shell, query cache interactive refresh, local form draft
29. Production Boundary Checklist
Before introducing RSC into a production workflow, answer these questions.
Runtime Boundary
- Which modules are server-only?
- Which modules are client-only?
- Which modules are truly shared?
- Is the import graph enforced?
- Are client islands as small as they can be without hurting interaction design?
Data Boundary
- What exact data crosses into Client Components?
- Is every prop serializable?
- Are raw domain/database objects blocked?
- Are secrets impossible to serialize accidentally?
- Are DTOs versionable?
Authorization Boundary
- Is authorization done before data access?
- Is every query scoped by tenant/account/org?
- Are UI permissions treated as hints, not enforcement?
- Are denied render attempts observable?
Performance Boundary
- Are server data dependencies batched?
- Are optional slow panels isolated?
- Are server render waterfalls traced?
- Are client island bundle sizes monitored?
- Does streaming improve meaningful UX, or just show skeleton noise?
Mutation Boundary
- What handles commands?
- How is server-rendered data revalidated?
- How are unknown mutation outcomes handled?
- Does the client have a clear pending/error state?
30. Summary
React Server Components change the topology of React client-server communication.
They let component execution happen on the server.
They let server-owned read models be composed closer to UI structure.
They can reduce client bundle size and client-side fetch waterfalls.
But they do not eliminate boundaries.
They introduce new ones:
- server module graph vs client module graph;
- serialized UI payload vs executable browser code;
- server-owned read model vs client-owned interaction;
- render-time data access vs command-time mutation;
- permission-filtered DTOs vs raw domain objects;
- server render workload vs browser runtime workload.
The key invariant:
A Server Component may run on the server, but anything it passes to a Client Component has crossed into the browser trust boundary.
That one sentence prevents most RSC architecture mistakes.
Part 040 builds on this by treating Server Functions and Actions as explicit client-triggered server execution boundaries.
References
- React Docs — Server Components: https://react.dev/reference/rsc/server-components
- React Docs —
"use client": https://react.dev/reference/rsc/use-client - React Docs — Directives: https://react.dev/reference/rsc/directives
- React Docs — Server Functions: https://react.dev/reference/rsc/server-functions
- React Docs —
use: https://react.dev/reference/react/use - React DOM —
hydrateRoot: https://react.dev/reference/react-dom/client/hydrateRoot
You just completed lesson 39 in build core. Use the series map if you want to review the broader track, or continue directly into the next lesson while the context is still warm.
Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.