Module: React and Ecosystem
React and Ecosystem·115·13 MIN READ

115: Server Rendering, Hydration, React Server Components, Security, and Production Capstone

TOPICS COVERED: 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 createRoot versus hydrateRoot;
  • 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.

text
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:

jsx
import {
  createRoot,
} from 'react-dom/client';

createRoot(root)
  .render(<App />);

Hydrating server HTML:

jsx
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:

jsx
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:

jsx
// Server Component
async function TaskPage() {
  const tasks =
    await db.tasks
      .findMany();

  return (
    <TaskWorkspace
      tasks={tasks}
    />
  );
}

Interactive client component:

jsx
'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:

jsx
'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:

jsx
'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:

text
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:

jsx
<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:

jsx
<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

text
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:

ValueOwner
routeReact Router
status/page filterURL
server tasksTanStack Query v5
edit draftRHF/form
sidebar preferencelocal/Redux depending scope
authenticated authorityserver + client projection
derived countsrender/select

No duplicated server list in Redux.

Required server-state behavior

Use v5 object syntax:

jsx
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

  1. Explain render versus commit versus hydration.
  2. Why is a Server Component not marked "use server"?
  3. What is a Server Function?
  4. Why can client route guards never authorize a mutation?
  5. When should server state live in TanStack Query versus a route loader?
  6. How would you choose an Error Boundary and Suspense boundary?
  7. What does React Compiler change about useMemo/useCallback strategy?
  8. How does optimistic rollback work in TanStack Query v5?
  9. Which state belongs in the URL?
  10. How would you debug a hydration mismatch?

Official references


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

text
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

text
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

text
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:

text
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:

text
Date.now()
Math.random()
browser-only conditional
locale difference
invalid HTML nesting
external DOM modification

Browser-only logic

Bad:

jsx
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;
  • useSyncExternalStore with 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:

text
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:

text
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:

jsx
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:

jsx
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:

jsx
'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:

text
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:

text
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:

text
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:

jsx
<p>{userText}</p>

React escapes.

Danger:

jsx
<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:

text
Who are you?

Authorization:

text
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:

text
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:

text
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

text
AppShell
├─ Navigation
├─ RouteBoundary
│  └─ TaskWorkspace
└─ ToastRegion

2. State ownership

text
URL → filters
Query → server tasks
RHF → edit draft
Redux/local → client selection/preferences
Server → authorization

3. Request flow

text
UI mutation
→ API/Server Function
→ auth/authz
→ validation
→ DB
→ response
→ Query invalidation/update
→ UI

4. Failure flow

text
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:

  1. What owns each state category?
  2. Why is render pure?
  3. When should an Effect exist?
  4. Why Query rather than Context for server state?
  5. Why Router rather than Redux for URL state?
  6. How does optimistic rollback preserve concurrent changes?
  7. What changes at a Server Component boundary?
  8. Why is a Server Function an authorization boundary?
  9. How would you debug hydration?
  10. 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:

jsx
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:

text
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:

text
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:

text
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:

text
"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:

text
static/pre-renderable shell

from:

text
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:

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