FullStack Course LogoFullStack Course
Module: React and Ecosystem
React and Ecosystem·115·23 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

The first decision is where the initial UI is produced. React can do that work in the browser, on the server for each request, or ahead of time. Streaming changes when pieces become visible, while hydration describes how already-rendered HTML becomes connected to React on the client. These ideas are related, but they are not interchangeable.

Client-side rendering (CSR)

With client-side rendering (CSR), the server returns an HTML shell and JavaScript builds the application UI in the browser.

text
HTML shell
↓
download JS
↓
run React
↓
render UI

The browser therefore has to download and execute enough JavaScript before the meaningful application content can appear. This model is often a good fit for authenticated tools and other browser-led applications, especially when simple static hosting is valuable.

Server-side rendering (SSR)

With server-side rendering (SSR), the server renders React to HTML for a particular request.

The browser receives meaningful HTML before the client JavaScript becomes interactive. That can improve the initial visible experience, but it does not remove the need for client JavaScript when the page contains interactive Client Components. The server and browser must also agree about the initial result.

Static rendering / prerendering

Static rendering, also called prerendering, generates HTML before requests arrive.

This works well for pages whose content is known at build or prerender time. The resulting artifacts can often be served efficiently from a CDN, but private, per-user data must not be baked into a public artifact.

Streaming SSR

Streaming SSR lets the server send portions of the HTML as the corresponding Suspense boundaries become ready.

Users can see useful content before the entire tree finishes rendering. That changes the design problem: loading fallbacks and the order in which regions arrive are now part of the user experience and accessibility work, not merely implementation details.

Streaming is still server rendering, not a separate way to make client code interactive. The browser can display an early response, but interactive boundaries still need the client-side work required by the application. Measure the result at the point users can use the page, not only at the point the first bytes arrive.

Client root versus hydration

There are two different ways to establish a React root. If React is responsible for creating the UI from an empty container, use a client-only root:

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

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

If the server has already produced the HTML, use hydration:

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

hydrateRoot(
  document,
  <App />,
);

Hydration attaches React behavior to HTML that already exists. It is not simply another spelling of initial rendering: React expects the client's initial output to correspond to the server output.

Do not call createRoot on server-rendered app HTML and expect hydration semantics. Doing so treats the existing markup as something to replace rather than markup to adopt.

That distinction also affects how you interpret a blank page or a flash of content during debugging. A client-only root may be waiting for JavaScript and data before it can render, while a hydration problem usually means HTML arrived but the client could not reconcile its initial view with it. Inspect the document, console, and Network panel before changing the root API.

Hydration mismatch

The server and client should agree on the initial output. If they do not, React may warn, replace parts of the markup, lose state, or attach events in an unexpected way.

For example, this component reads the current time while rendering:

jsx
function Clock() {
  return (
    <p>
      {new Date()
        .toLocaleTimeString()}
    </p>
  );
}

The server's time and the client's hydration time can differ, even when the component code is identical. The output is therefore not deterministic across the two environments.

Other mismatch causes include:

  • 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 a mismatch until you understand its cause. The warning is often the first useful clue that server and client state, environment, or markup have diverged.

When debugging, compare the server's serialized inputs and rendered markup with the client's initial inputs and render path. Check nondeterministic values, locale and time-zone assumptions, browser-only branches, and any code that mutates the DOM before hydration. Fix the source of the divergence rather than hiding a symptom.

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, because a Server Component does not represent a persistent interactive instance in the browser.

Here is the basic shape of a Server Component that reads data and passes it to an interactive part of the UI:

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

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

The receiving component can opt into client capabilities:

jsx
'use client';

import {
  useState,
} from 'react';

export default function
TaskWorkspace({
  tasks,
}) {
  const [
    selectedId,
    setSelectedId,
  ] =
    useState(null);

  ...
}

The boundary is useful precisely because the two components have different responsibilities: the page can obtain trusted server data, while the workspace owns browser interaction.

This does not mean every server-rendered page must use RSC. RSC is an architecture available in supported environments, with its own serialization, caching, and security boundaries. The important skill is knowing which work benefits from server execution and which work genuinely needs a browser instance.

"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 may use state, Effects, event handlers, browser APIs, and similar capabilities.

That boundary has a cost: the relevant dependency graph becomes eligible for client-side delivery. Put the boundary where interaction begins rather than placing it at the root by default.

"use server"

This distinction is a frequent source of confusion:

"use server" marks Server Functions.

It does not mark a Server Component. Server Components do not require a "use server" directive.

A Server Function is an explicit server-side mutation boundary. For example:

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 an endpoint-like server boundary even when the framework makes the call feel like a function call. Treat every input as untrusted and apply the same discipline you would apply to a public mutation endpoint.

Server Function security

Never assume that a function is authorized just because the call originated in your React UI. Client code can be changed or called outside the intended UI.

A Server Function must verify:

  • authentication;
  • authorization;
  • input validation;
  • tenancy/ownership;
  • CSRF/origin strategy where relevant;
  • rate limits for sensitive workflows;
  • safe error disclosure.

The server is the trust boundary. A hidden button can improve the experience for an unauthorized user, but it cannot enforce access control.

RSC data flow

In supported frameworks, a Server Component can start a promise and pass it to a Client Component. The client component can read that promise with use, while a Suspense boundary supplies the loading experience.

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 around this work. Do not create uncached promises repeatedly in client render. Repeated promise creation can prevent stable resource identity and make the loading behavior incorrect.

The promise itself is a handoff of data work, not permission to move all server responsibilities into the browser. Keep database access, credentials, and authorization on the server. The Client Component should receive only the supported data or promise representation that it needs to render its part of the interface.

Serializability boundary

Props crossing from Server Components to Client Components must satisfy the React/framework serialization contract.

Do not pass arbitrary browser or server objects, open database connections, or non-serializable instances across the boundary. Keep environment-specific capabilities on the side where they belong and pass the data the other side actually needs.

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 APIs correctly rather than hand-building an SSR server as a first step. The course teaches the underlying model so framework behavior remains understandable instead of appearing to be magic.

Partial prerendering in React 19.2

React 19.2 adds lower-level capabilities for prerendering static content and resuming dynamic rendering.

The architecture is the part to understand:

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. Choose the simplest architecture that meets the application's delivery and data requirements.

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 a collection of unexplained magic. Map each feature back to:

  • route ownership;
  • server/client boundary;
  • Suspense;
  • hydration;
  • data ownership;
  • authorization.

React Server Component security

RSC and Server Function infrastructure runs privileged code. Keep its dependencies patched and follow React and 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. Transport changes how a value arrives; it does not make the value authoritative.

The same rule applies to values that appear in a form, URL, hidden field, or serialized action argument. The UI can suggest the user's identity, tenant, or allowed operation, but only the server can establish those facts for a protected mutation.

XSS

React escapes string children by default:

jsx
<p>{userText}</p>

That default is safer than injecting HTML because text is treated as text rather than parsed as markup.

dangerouslySetInnerHTML bypasses that protection. If product requirements genuinely require user-supplied HTML, sanitize it with a security-reviewed approach. Do not sanitize with regex; HTML parsing and browser contexts are more complicated than pattern replacement can safely handle.

URLs

Validate user-controlled URLs before placing them in sensitive navigation or resource contexts.

Do not treat this as automatically safe in every product threat model:

jsx
<a href={userValue}>

The allowed protocol, destination, context, and product threat model all matter.

Authentication state

Client authentication state controls UX. Server authentication controls access.

A hidden Delete button is not a permission check, and a ProtectedRoute is not authorization. Every protected API or Server Function must verify the user and the target resource on the server.

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.

These checks apply to server-rendered and client-rendered interfaces alike. SSR can improve when content appears, but it does not automatically make an application accessible.

Observability

Production errors need enough context to reconstruct what happened without turning telemetry into a data leak.

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. The goal is not only to make the happy path work; the architecture should make ownership, failure handling, security, accessibility, and measurement explicit.

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 and give it one clear owner:

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 belongs in Redux. Duplicate sources of truth make invalidation and synchronization harder to reason about.

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.

Client validation can provide fast guidance, but server validation remains authoritative. Pending and duplicate-submit behavior should make the current operation clear rather than allowing competing mutations.

When a mutation fails, the form should preserve enough of the user's draft to recover. Map authoritative field errors back to the relevant controls, move focus to the first actionable problem where appropriate, and keep transport failures distinct from validation failures.

Required routing behavior

Include:

  • nested routes;
  • not-found;
  • URL filters;
  • route error boundary;
  • protected UI;
  • direct server authorization.

Protected UI is useful for navigation and feedback. Direct server authorization is what protects the operation.

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.

Tests should exercise the boundaries, not only the component's successful render. A useful test proves which state is retained after a failed mutation, which boundary handles a route error, and what a user can still do while the network is slow or unavailable.

Required performance evidence

Use a production build and the Profiler.

Show:

  • one measured interaction;
  • before/after evidence if you optimize;
  • bundle inspection;
  • explanation of whether manual memoization is still needed with compiler strategy.

Claims about performance are not evidence. Record what was measured, under what conditions, and whether the change improved the user-visible interaction.

Also separate delivery work from runtime work. A smaller transfer can help, but parsing, scripting, rendering, layout, paint, and image decoding can still dominate the interaction. The profiler and bundle inspection should support the explanation rather than serve as decorative evidence.

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. In particular, do not let a client boundary obscure which server operation checks the current user and resource.

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 or RSC simply because they are “more modern.” Start with the users, data, deployment environment, and trust boundaries the application actually has.

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?

There is no universal winner among these rendering modes. A public product page and an internal task dashboard may have completely different best choices even when both are built with React. The right decision balances first content, interactivity, operational cost, data freshness, caching, and the confidentiality of the data being rendered.

Write down the boundary before implementation: which work happens during a build, which work happens for each request, and which work must wait for the browser. That small exercise makes later framework defaults easier to evaluate and makes performance or security trade-offs visible.

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.

It is not suitable for per-user confidential data baked into public artifacts.

Streaming

Streaming allows the server to send the shell and completed Suspense regions while slower parts continue.

A useful layout might look like this:

text
Header/navigation → immediate
Product summary → immediate
Recommendations → stream later
Reviews → stream later

Fallback design matters because users see the streamed sequence. A fallback should communicate what is pending without causing layout instability or unnecessary noise for assistive technology.

Streaming does not guarantee a faster complete page. It can improve perceived progress when the shell and fast regions are useful on their own, but a slow server dependency may still determine when the task is complete. Measure both early visibility and completion of the interaction users came to perform.

Hydration identity

The server and client must agree on the initial element structure.

Mismatches can cause:

  • warning;
  • client replacement;
  • lost state;
  • unexpected event attachment.

Common sources include:

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

Browser-only logic

This component reads a browser API during render:

Bad:

jsx
function Theme() {
  const dark = window.matchMedia('(prefers-color-scheme: dark)').matches;
  return <div>{dark ? 'dark' : 'light'}</div>;
}

It crashes on the server because window does not exist there.

Options include:

  • CSS media queries for presentation;
  • server-known preference;
  • useSyncExternalStore with server snapshot;
  • client enhancement after hydration.

Choose based on the requirement. If this is purely presentation, CSS may be the cleanest answer. If the server must produce a consistent initial value, provide that value explicitly. If the browser is the only authority, design the post-hydration enhancement and its loading state deliberately.

Client boundary cost

"use client" marks a module boundary whose dependencies become part of the client-side capability and bundle graph.

Do not put it at the root just because one leaf needs useState. Prefer a narrow boundary:

text
Server page
├─ Server header
├─ Server product details
└─ Client AddToCartButton

rather than turning the whole page into client code. A smaller client graph can reduce JavaScript while keeping only the interactive control in the browser.

The boundary is also a maintenance boundary. Imports, props, and data-fetching assumptions crossing it deserve review because they affect bundle size, serialization, and where failures occur. Narrow boundaries are not a rule to apply mechanically, but they make those costs easier to see.

Serialization

Crossing the server-to-client boundary requires supported serializable values.

Do not pass:

text
DB connection
secret service instance
raw Request object
arbitrary class instance
function (unless supported Server Function reference)

Pass data instead. Reconstruct client behavior from that data on the client, and retain privileged capabilities on the server.

Server Components do not have lifecycle Effects

A Server Component renders on the server. It cannot use:

jsx
useState
useEffect
DOM refs
browser events

There is no persistent browser instance for that component. It can, however, compose Client Components where interactivity is needed.

Data access in Server Components

A 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} />;
}

Depending on the framework architecture, this can avoid a separate browser API call for the initial data. Authorization is still required before reading. Server execution is not a substitute for access control.

Server Functions deep security

A Server Function callable from the client is conceptually a network-exposed mutation entry point. Treat its 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 values such as:

text
userId
tenantId
price
role
permission

when the server can derive them. Validate the input, identify the authenticated principal on the server, and check access to the specific resource.

CSRF and origin concerns

Cookie-authenticated server mutations need an appropriate CSRF or origin strategy supplied by the framework and platform architecture.

Do not assume that “React Action” automatically eliminates CSRF considerations. Follow the framework's security documentation and understand which checks are actually applied at the mutation boundary.

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 need different caching strategies; do not let a convenient cache default decide that boundary for you.

When reviewing a cache, test two different identities and two different tenants, not just two requests from the same user. Confirm that authorization changes, locale differences, and expired data produce the intended result. A fast response is not a correct response if it came from the wrong security scope.

RSC package security

Server Component protocols and tooling have received security-sensitive updates in the ecosystem.

Keep React and framework versions patched. Treat server serialization and deserialization boundaries as security-critical infrastructure. Do not pin old vulnerable framework versions for tutorial reproducibility without an explicit warning.

Hydration and authentication

The server may render:

text
Logged in as Alice

while the client authentication store initializes as unauthenticated. That mismatch can flash incorrect UI.

When using SSR, provide consistent initial authentication state from the server, or design an intentional loading boundary. Do not let two independent initialization paths briefly claim different identities.

TanStack Query hydration

In SSR applications, a framework can prefetch queries on the server and hydrate the query cache on the client.

The conceptual flow is:

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. Server cache lifetime and user isolation must be deliberate.

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 a framework change does not destroy your understanding of what runs where or who owns the data.

XSS deep dive

The safe default is still text children:

jsx
<p>{userText}</p>

React escapes that value.

This is dangerous when html is not trusted:

jsx
<div dangerouslySetInnerHTML={{ __html: html }} />

If HTML is user-controlled, sanitize it with a proven sanitizer and 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 asks:

text
Who are you?

Authorization asks:

text
May you perform this operation on this resource?

A client route guard can help with authentication UX. A server mutation must authorize the resource, and a multi-tenant server query must always scope the tenant on the server.

Do not trust a tenant ID from a route or request body alone. Derive the relevant authority from the authenticated server context and verify the target resource.

Secret handling

Never send server secrets to Client Components through 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 or server-only environment configuration instead.

CSP

HTML introduced Content Security Policy. Production React applications should use CSP where the architecture supports it to reduce the impact of XSS.

Be aware that these can affect CSP configuration:

  • inline scripts/styles;
  • framework streaming/bootstrap;
  • third-party analytics.

Use nonces, hashes, and framework guidance rather than disabling the policy with broad unsafe-inline unless that choice is 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. Navigation and streamed updates still need an intentional focus and announcement strategy.

Observability and Error Boundaries

A client Error Boundary can report:

text
release
route
component stack
correlation/request ID

Server logs can report the same request ID. Correlating both sides reduces the time needed to follow a failure from the browser to the server.

Do not send sensitive form data in error telemetry.

Deployment correctness

Test all of the following:

  • 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 application that works only through development-server navigation is not production-ready. Direct requests, deployment configuration, and server lifecycle behavior are part of the application.

Run these checks against the production build and its real hosting configuration. Development servers often provide fallback routing, unoptimized assets, permissive headers, and different timeout behavior, so a successful local navigation is not proof that deployment is correct.

Capstone architecture review

Before coding, create four diagrams. They force the important ownership decisions into the open before implementation details obscure them.

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 or failure handling cannot be drawn clearly, the architecture is not finished. The diagrams should explain what happens, not merely name the libraries involved.

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 each of these conditions:

  • 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 down what the user sees and what the system logs for each case. This connects resilience testing to both product behavior and operational debugging.

Final mastery questions

You should be able to answer these 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 those APIs participate in.

Use the capstone to demonstrate the answers, not just recite them. A good review can point from each visible behavior to its owner, its server boundary, its failure path, and the test or measurement that supports the design.


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 or cache lifetime.

Here is a 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 useful mental model is not “cache every fetch.”

cache() can memoize or deduplicate a server function within React's cache behavior.

cacheSignal() provides abort and lifetime information so underlying asynchronous work can stop when React no longer needs the cached result, for example when rendering is aborted or the cache lifetime ends.

That is relevant for expensive server work such as:

text
database wrapper that supports AbortSignal
internal HTTP request
long-running data transformation
server resource acquisition

The abort signal is useful only when the underlying operation observes it. Passing a signal to a function that cannot cancel its work does not make that work abortable; it merely documents a boundary that the implementation still needs to honor.

cache() is not your complete application cache

Do not confuse React's cache with any of these:

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 these questions:

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 or user data is a security defect, not merely a stale-data bug.

The word “cache” therefore needs a scope attached to it. A per-render deduplication mechanism, a request cache, a process-level cache, and a shared CDN cache have very different isolation properties. Review the scope before deciding that two equal function calls are safe to reuse.

Request-scoped authority still matters

Even when a data function is cached, authorization must happen in the correct trusted scope.

Do not cache this decision globally:

text
"current user may edit order 42"

unless every security-relevant identity and input is included and the framework cache semantics are fully understood. Prefer an 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 and 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.

Reader page: /react/lesson/115/server-rendering-hydration-react-server-components-security-and-production-capstone