115: Server Rendering, Hydration, React Server Components, Security, and Production Capstone
Learning objectives
You will learn to:
- distinguish CSR, SSR, SSG/static prerendering, streaming, and hydration;
- understand
createRootversushydrateRoot; - understand Server Component and Client Component boundaries;
- correctly distinguish
"use client"and"use server"; - understand Server Functions;
- use Suspense with streaming/server data;
- recognize hydration mismatch causes;
- understand that frameworks usually orchestrate RSC/SSR APIs;
- apply production security and accessibility boundaries;
- complete a final architecture capstone using the entire React module.
Rendering modes
Client-side rendering (CSR)
The server sends a shell and JavaScript builds the UI in the browser.
HTML shell ↓ download JS ↓ run React ↓ render UI
Server-side rendering (SSR)
The server renders React to HTML for a request.
The browser receives meaningful HTML before client JavaScript becomes interactive.
Static rendering / prerendering
HTML is generated ahead of requests.
Useful for pages whose content can be known at build/prerender time.
Streaming SSR
The server can stream pieces of the HTML as Suspense boundaries become ready.
Users can see useful content before the entire tree finishes.
Client root versus hydration
Client-only root:
import {
createRoot,
} from 'react-dom/client';
createRoot(root)
.render(<App />);
Hydrating server HTML:
import {
hydrateRoot,
} from 'react-dom/client';
hydrateRoot(
document,
<App />,
);
Hydration attaches React behavior to HTML that already exists.
Do not call createRoot on server-rendered app HTML and expect hydration semantics.
Hydration mismatch
Server and client initial output should agree.
Problem:
function Clock() {
return (
<p>
{new Date()
.toLocaleTimeString()}
</p>
);
}
The server time and client hydration time can differ.
Other mismatch causes:
- browser-only APIs during server render;
- random values;
- invalid HTML nesting;
- different locale/time zone;
- conditional branches based on
window; - extensions modifying HTML.
Do not silence mismatches without understanding the cause.
Server Components
Server Components execute in a server environment and are not sent to the browser as interactive component JavaScript.
They can:
- access server-side data sources;
- use async/await in supported RSC environments;
- render non-interactive UI;
- compose Client Components.
They cannot use browser interaction Hooks such as useState.
Conceptual example:
// Server Component
async function TaskPage() {
const tasks =
await db.tasks
.findMany();
return (
<TaskWorkspace
tasks={tasks}
/>
);
}
Interactive client component:
'use client';
import {
useState,
} from 'react';
export default function
TaskWorkspace({
tasks,
}) {
const [
selectedId,
setSelectedId,
] =
useState(null);
...
}
"use client"
"use client" defines a client boundary in an RSC-aware environment.
It does not mean:
this code can only ever render in the browser.
Frameworks can still server-render client component HTML and hydrate it.
The directive marks the module and its dependency subtree as client-capable code that can use state, Effects, event handlers, browser APIs, etc.
"use server"
Important:
"use server" marks Server Functions.
It does not mark a Server Component.
Server Components do not require a "use server" directive.
Server Function:
'use server';
export async function
createTask(
formData,
) {
const session =
await requireSession();
const title =
String(
formData.get(
'title',
)
?? '',
).trim();
if (title.length < 3) {
return {
error:
'Title too short',
};
}
return db.task
.create({
ownerId:
session.userId,
title,
});
}
A Server Function is a server endpoint-like boundary.
Treat all inputs as untrusted.
Server Function security
Never assume that because a function was called from your React UI it is authorized.
Server Function must verify:
- authentication;
- authorization;
- input validation;
- tenancy/ownership;
- CSRF/origin strategy where relevant;
- rate limits for sensitive workflows;
- safe error disclosure.
Client code can be modified by the attacker.
The server is the trust boundary.
RSC data flow
A Server Component can start a promise and pass it to a Client Component in supported frameworks.
Client:
'use client';
import {
use,
} from 'react';
function Comments({
commentsPromise,
}) {
const comments =
use(
commentsPromise,
);
return (
<ul>
{comments.map(
(comment) => (
<li
key={
comment.id
}
>
{
comment.body
}
</li>
),
)}
</ul>
);
}
Suspense can provide the loading boundary.
Do not create uncached promises repeatedly in client render.
Serializability boundary
Props crossing from Server Components to Client Components need to obey the framework/React serialization contract.
Do not try to pass arbitrary browser/server objects, open database connections, or non-serializable instances across the boundary.
Keep environment-specific capabilities on the correct side.
React DOM server APIs
React provides server APIs such as:
renderToReadableStream;renderToPipeableStream;- static/prerender APIs.
Most application teams should use a framework that orchestrates these correctly rather than hand-building an SSR server first.
This course teaches the model so framework behavior is understandable.
Partial prerendering in React 19.2
React 19.2 adds lower-level capabilities for prerendering static content and resuming dynamic rendering.
You should understand the architecture:
pre-render static shell ↓ serve/cache shell ↓ resume dynamic rendering ↓ stream remaining content
Application frameworks may expose higher-level versions of this idea.
Do not implement low-level partial-prerender infrastructure in a beginner application merely because the API exists.
Framework boundaries
React itself is a UI library/runtime.
Production React frameworks can add:
- filesystem/route modules;
- SSR;
- streaming;
- RSC;
- server functions;
- static generation;
- metadata handling;
- deployment integration.
Examples in the ecosystem include framework-oriented React Router setups and Next.js.
Do not learn a framework as magic. Map framework features back to:
- route ownership;
- server/client boundary;
- Suspense;
- hydration;
- data ownership;
- authorization.
React Server Component security
RSC/server function infrastructure runs privileged code.
Keep dependencies patched and follow React/framework security advisories.
Do not expose:
- environment secrets;
- database credentials;
- internal stack traces;
- serialized privileged objects.
Do not trust action arguments merely because React transported them.
XSS
React escapes string children by default:
<p>{userText}</p>
This is safer than injecting HTML.
dangerouslySetInnerHTML bypasses that protection.
If product requirements truly need user-supplied HTML, sanitize using a security-reviewed approach.
Do not sanitize with regex.
URLs
Validate user-controlled URLs before placing them in sensitive navigation/resource contexts.
Do not treat:
<a href={userValue}>
as automatically safe in every product threat model.
Authentication state
Client auth state controls UX.
Server auth controls access.
A hidden Delete button is not a permission check.
A ProtectedRoute is not authorization.
Every protected API/Server Function must verify the user and target resource.
Accessibility production checklist
Before release verify:
- keyboard-only core journeys;
- visible focus;
- semantic headings/landmarks;
- form labels and error relations;
- dialogs and focus return;
- color contrast;
- 200% zoom/reflow;
- reduced motion;
- loading/error announcements;
- route-title/focus behavior where needed.
Observability
Production errors require context.
Capture:
- component/route;
- request correlation ID;
- operation;
- safe error code;
- release version;
- browser/runtime;
- user/tenant identifier only when policy permits.
Do not log:
- passwords;
- auth tokens;
- full payment data;
- sensitive personal content.
Final capstone: Task Workspace
Build or refactor the canonical task manager into a production-style Task Workspace.
Required architecture
Router ├─ App shell │ ├─ authenticated account boundary │ └─ route outlet │ ├─ /tasks │ ├─ URL-owned filters/page │ ├─ TanStack Query v5 task data │ ├─ accessible TaskForm │ └─ optimistic mutations │ ├─ /tasks/:taskId │ ├─ details query │ ├─ edit form │ └─ error/loading boundary │ └─ /settings └─ client preference state
Required ownership table
Document every value:
| Value | Owner |
|---|---|
| route | React Router |
| status/page filter | URL |
| server tasks | TanStack Query v5 |
| edit draft | RHF/form |
| sidebar preference | local/Redux depending scope |
| authenticated authority | server + client projection |
| derived counts | render/select |
No duplicated server list in Redux.
Required server-state behavior
Use v5 object syntax:
useQuery({
queryKey: ...,
queryFn: ...,
});
useMutation({
mutationFn: ...,
});
Include:
- loading;
- empty;
- error;
- retry;
- invalidation;
- optimistic rollback;
- cancellation where relevant.
Required form behavior
Include:
- client guidance;
- server validation;
- field error mapping;
- pending UI;
- focus after error;
- duplicate-submit handling.
Required routing behavior
Include:
- nested routes;
- not-found;
- URL filters;
- route error boundary;
- protected UI;
- direct server authorization.
Required testing evidence
Provide:
- reducer/store unit tests where used;
- component tests;
- MSW network tests;
- optimistic rollback test;
- route error test;
- one accessibility scan plus manual checklist;
- one Playwright critical journey.
Required performance evidence
Use production build and Profiler.
Show:
- one measured interaction;
- before/after evidence if you optimize;
- bundle inspection;
- explanation of whether manual memoization is still needed with compiler strategy.
Required resilience
Test:
- slow network;
- offline request;
- 401/403;
- 404;
- 409 or 422;
- 500;
- stale response;
- aborted navigation;
- duplicate submit;
- zero tasks;
- large task list.
Optional advanced extension
In a framework that supports RSC:
- render task shell/data on the server;
- keep interactive editor as a Client Component;
- submit through a Server Function/Action;
- stream a secondary panel through Suspense.
Explain exactly where authentication and authorization execute.
Final interview questions
- Explain render versus commit versus hydration.
- Why is a Server Component not marked
"use server"? - What is a Server Function?
- Why can client route guards never authorize a mutation?
- When should server state live in TanStack Query versus a route loader?
- How would you choose an Error Boundary and Suspense boundary?
- What does React Compiler change about
useMemo/useCallbackstrategy? - How does optimistic rollback work in TanStack Query v5?
- Which state belongs in the URL?
- How would you debug a hydration mismatch?
Official references
- https://react.dev/reference/react-dom/client/hydrateRoot
- https://react.dev/reference/react-dom/server
- https://react.dev/reference/rsc/server-components
- https://react.dev/reference/rsc/use-client
- https://react.dev/reference/rsc/use-server
- https://react.dev/reference/react/use
- https://react.dev/reference/react/Suspense
- https://react.dev/blog/2025/10/01/react-19-2
Deep dive: choose rendering architecture by user and deployment needs
Do not choose SSR/RSC because it is "more modern."
Ask:
- Is first-content speed important?
- Is content public/SEO-sensitive?
- Is app authenticated/internal?
- How dynamic is data?
- Can edge/CDN cache it?
- How much client interactivity?
- What deployment platform?
- What security boundary?
- Does team need server components complexity?
CSR architecture
request HTML shell → download JS → run React → fetch data → show UI
Advantages:
- simple static hosting;
- clean browser app model;
- great for many authenticated tools.
Trade-offs:
- slower meaningful first render on poor network;
- SEO/meta may need extra work;
- client bundle carries more work.
SSR architecture
request → server fetch/render → HTML → browser displays → JS loads → hydration
Advantages:
- meaningful HTML earlier;
- route-specific server data;
- SEO/public content.
Trade-offs:
- server complexity;
- hydration cost;
- request rendering cost;
- server/client consistency constraints.
Static prerender
build/revalidation → generate HTML → CDN
Excellent for:
- documentation;
- marketing;
- product catalog pages that can tolerate revalidation.
Not suitable for per-user confidential data baked into public artifacts.
Streaming
Streaming allows server to send shell and completed Suspense regions while slower parts continue.
A useful layout:
Header/navigation → immediate Product summary → immediate Recommendations → stream later Reviews → stream later
Fallback design matters because users see the streamed sequence.
Hydration identity
Server and client must agree on initial element structure.
Mismatches can cause:
- warning;
- client replacement;
- lost state;
- unexpected event attachment.
Sources:
Date.now() Math.random() browser-only conditional locale difference invalid HTML nesting external DOM modification
Browser-only logic
Bad:
function Theme() {
const dark = window.matchMedia('(prefers-color-scheme: dark)').matches;
return <div>{dark ? 'dark' : 'light'}</div>;
}
This crashes on server.
Options:
- CSS media queries for presentation;
- server-known preference;
useSyncExternalStorewith server snapshot;- client enhancement after hydration.
Choose based on requirement.
Client boundary cost
"use client" marks a module boundary whose dependencies become part of client-side capability/bundle graph.
Do not put it at the root just because one leaf needs useState.
Prefer:
Server page ├─ Server header ├─ Server product details └─ Client AddToCartButton
rather than turning whole page client.
This can reduce JS.
Serialization
Crossing server→client boundary requires serializable supported values.
Do not pass:
DB connection secret service instance raw Request object arbitrary class instance function (unless supported Server Function reference)
Pass data.
Server Components do not have lifecycle Effects
A Server Component renders on server.
It cannot:
useState
useEffect
DOM refs
browser events
because there is no persistent browser instance for it.
It can compose Client Components for interactivity.
Data access in Server Components
Server component can access trusted server modules:
async function OrdersPage() {
const user = await requireUser();
const orders = await db.orders.findForUser(user.id);
return <OrdersView orders={orders} />;
}
This can avoid exposing a separate browser API call for initial data, depending framework architecture.
But authorization is still required before reading.
Server Functions deep security
A Server Function callable from client is conceptually a network-exposed mutation entry point.
Treat parameters as hostile:
'use server';
export async function updateOrder(input) {
const user = await requireUser();
const parsed = schema.parse(input);
const order = await db.orders.findById(parsed.id);
if (!order || order.tenantId !== user.tenantId) {
throw new ForbiddenError();
}
...
}
Never trust client-provided:
userId tenantId price role permission
if server can derive them.
CSRF and origin concerns
Cookie-authenticated server mutations need an appropriate CSRF/origin strategy as provided by framework/platform architecture.
Do not assume "React Action" automatically means no CSRF considerations.
Follow framework security documentation.
Cache security
Server caching must vary correctly by:
- user;
- tenant;
- locale;
- authorization;
- request headers where needed.
A cache key mistake can leak one user's private data to another.
Public static data and per-user private data require different caching strategies.
RSC package security
Server Component protocol/tooling has had security-sensitive updates in the ecosystem.
Keep React/framework versions patched.
Treat server serialization/deserialization boundaries as security-critical infrastructure.
Do not pin old vulnerable framework versions for tutorial reproducibility without warning.
Hydration and authentication
Server may render:
Logged in as Alice
but client auth store initializes as unauthenticated.
That mismatch can flash incorrect UI.
Provide consistent initial auth state from server when using SSR, or design loading boundary intentionally.
TanStack Query hydration
In SSR apps, a framework can prefetch queries server-side and hydrate query cache client-side.
Conceptual flow:
server QueryClient → prefetch → dehydrate → serialize safe cache state → client HydrationBoundary → query observers reuse data
Be careful with:
- request-scoped QueryClient;
- sensitive cache serialization;
- staleTime;
- error serialization.
Do not share one server QueryClient across users.
Router/framework integration
React Router Framework Mode or Next.js can orchestrate:
- route modules;
- loaders/server data;
- streaming;
- RSC/SSR depending framework;
- error boundaries;
- metadata.
Learn the underlying ownership model so framework changes do not destroy understanding.
XSS deep dive
Safe default:
<p>{userText}</p>
React escapes.
Danger:
<div dangerouslySetInnerHTML={{ __html: html }} />
If HTML is user-controlled, sanitize with a proven sanitizer/configuration.
Also consider:
- URL protocols;
- SVG;
- CSS injection contexts;
- third-party embeds.
React escaping is context-specific protection, not a complete application security system.
Authentication versus authorization
Authentication:
Who are you?
Authorization:
May you perform this operation on this resource?
Client route guard can help authentication UX.
Server mutation must authorize resource.
Multi-tenant server query must always scope tenant server-side.
Do not trust tenant ID from route body alone.
Secret handling
Never send server secrets to Client Components via props.
Never put secrets in:
VITE_* NEXT_PUBLIC_* client bundle config HTML data attributes
Public environment variables are public.
Use server-side secret stores/env.
CSP
HTML module introduced Content Security Policy.
Production React apps should use CSP where architecture supports it to reduce XSS impact.
Be aware that:
- inline scripts/styles;
- framework streaming/bootstrap;
- third-party analytics;
affect CSP configuration.
Use nonces/hashes/framework guidance rather than disabling policy with broad unsafe-inline unless explicitly justified.
Accessibility in streaming/navigation
When route content changes:
- update document title;
- ensure focus/context;
- pending status should be understandable;
- skeletons should not create noisy accessibility trees;
- errors should be announced appropriately.
SSR does not automatically make an app accessible.
Observability and Error Boundaries
Client Error Boundary can report:
release route component stack correlation/request ID
Server logs can report same request ID.
Correlating both sides reduces debugging time.
Do not send sensitive form data in error telemetry.
Deployment correctness
Test:
- direct nested route request;
- refresh on route;
- asset base path;
- chunk cache after deploy;
- CSP;
- compression;
- source maps policy;
- environment config;
- health checks;
- server timeouts;
- graceful shutdown.
A React app that works only through dev server navigation is not production-ready.
Capstone architecture review
Before coding, create four diagrams.
1. Component tree
AppShell ├─ Navigation ├─ RouteBoundary │ └─ TaskWorkspace └─ ToastRegion
2. State ownership
URL → filters Query → server tasks RHF → edit draft Redux/local → client selection/preferences Server → authorization
3. Request flow
UI mutation → API/Server Function → auth/authz → validation → DB → response → Query invalidation/update → UI
4. Failure flow
422 → field errors 401 → login/session handling 403 → permission UI 404 → route/resource not found 409 → conflict 500 → error boundary/toast + monitoring offline → retry/keep draft
If ownership/failure cannot be drawn clearly, architecture is not done.
Capstone acceptance criteria expansion
Core React
Demonstrate:
- pure components;
- stable keys;
- local state;
- reducer/context where justified;
- refs only for escape hatches;
- Effect cleanup;
- no unnecessary derived-state Effects.
Router
Demonstrate:
- URL-owned shareable filters;
- nested routes;
- direct-load correctness;
- route data/error handling.
Query v5
Demonstrate:
- object syntax;
- key factory;
- staleTime rationale;
- cancellation;
- mutation invalidation;
- optimistic rollback;
- pagination/infinite query if domain needs it.
Forms
Demonstrate:
- accessible labels;
- client/schema validation;
- authoritative server validation;
- conflict handling;
- draft preservation.
Security
Demonstrate:
- no client-secret exposure;
- server auth/authz;
- sanitized HTML policy;
- correct tenant scoping;
- security headers/CSP plan.
Testing
Demonstrate:
- reducer/pure logic unit tests;
- component tests;
- MSW;
- router;
- Query;
- accessibility;
- E2E;
- concurrency/failure.
Performance
Demonstrate measured evidence, not claims.
Final failure-injection day
Before declaring the project done, deliberately cause:
- API 500;
- slow 5 s response;
- offline;
- duplicate click;
- 422;
- 403;
- 409;
- stale optimistic response;
- route lazy chunk failure if possible;
- hydration mismatch in test environment;
- 10k records;
- keyboard-only use;
- reduced motion;
- 200% zoom.
Write what the user sees and what the system logs for each.
Final mastery questions
You should be able to answer in architecture terms:
- What owns each state category?
- Why is render pure?
- When should an Effect exist?
- Why Query rather than Context for server state?
- Why Router rather than Redux for URL state?
- How does optimistic rollback preserve concurrent changes?
- What changes at a Server Component boundary?
- Why is a Server Function an authorization boundary?
- How would you debug hydration?
- How do you prove a performance optimization worked?
If you can answer those with concrete examples, you are not merely familiar with React APIs—you understand the system.
React 19.2 server depth: cache, cacheSignal, and abortable cached work
React Server Components can use React's server cache primitives to deduplicate work during a render/cache lifetime.
Conceptual example:
import {
cache,
cacheSignal,
} from 'react';
const getProduct = cache(
async (productId) => {
const response = await fetch(
`https://internal.example/products/${productId}`,
{
signal: cacheSignal(),
},
);
if (!response.ok) {
throw new Error(
`Product request failed: ${response.status}`,
);
}
return response.json();
},
);
async function ProductPage({ productId }) {
const product = await getProduct(productId);
return <ProductDetails product={product} />;
}
The important mental model is not "cache every fetch."
cache() can memoize/deduplicate a server function within React's cache behavior.
cacheSignal() gives abort/lifetime information so underlying async work can stop when React no longer needs the cached result—for example when rendering is aborted or the cache lifetime ends.
This matters for expensive server work:
database wrapper that supports AbortSignal internal HTTP request long-running data transformation server resource acquisition
cache() is not your complete application cache
Do not confuse React cache with:
CDN cache HTTP Cache-Control database cache Redis TanStack Query browser cache framework route cache
Each has a different scope and invalidation model.
Before caching server work, answer:
Who shares this cache? How long? Can data be user-specific? Can tenant-specific values cross request boundaries? How is invalidation handled? What happens on authorization changes?
A cache that accidentally shares private tenant/user data is a security defect.
Request-scoped authority still matters
Even if a data function is cached, authorization should be performed in the correct trusted scope.
Do not cache:
"current user may edit order 42"
globally without including every security-relevant identity/input and understanding framework cache semantics.
Prefer server architecture where authorization decisions remain explicit and correctly scoped.
Partial pre-rendering connection
React 19.2's server DOM APIs support prerender/resume-style capabilities that frameworks can use to separate:
static/pre-renderable shell
from:
dynamic request-time content
This can improve delivery while retaining dynamic areas.
Application developers should usually consume this through a framework instead of directly assembling low-level streaming/resume infrastructure.
The course objective is to understand what the framework is coordinating:
pre-render cache/serve shell resume dynamic work stream Suspense regions hydrate/client-enable interactive boundaries
rather than treating SSR as one all-or-nothing renderToString operation.
