Angular / Advanced / Lesson 17
Server rendering and hydration boundaries
Choose a rendering strategy deliberately, preserve matching initial structure, and keep browser-only resources and request-specific state within their correct owners.
On this page
Lesson goals
- Distinguish client rendering, server rendering, prerendering, and hydration.
- Avoid browser-global access during server execution.
- Test request isolation and initial-state consistency separately from local models.
Rendering modes solve different delivery problems
Client rendering builds the view in the browser, server rendering generates a response on the server, and prerendering produces HTML ahead of requests. Hydration lets the browser reuse compatible server-rendered structure rather than recreate it blindly. These modes have different freshness, hosting, resource, and security requirements. A route requiring personalized data cannot be treated as a universally cacheable static page merely because its template is valid. Choose the supported Angular 20 rendering setup from the versioned documentation. The local examples in this course do not start an SSR server or claim measured search visibility or rendering performance.
Do not keep one mutable user state for every request
A module-level mutable object can survive across server requests and leak one user’s state into another rendering. The complete model creates separate signal state through a factory and changes only one owner. It verifies independent local allocations, not Angular’s full SSR injector behavior. In a real server application, keep request-sensitive providers and serialized data scoped correctly and test with two synthetic actors. A root service lifetime depends on how the application is bootstrapped; do not replace request isolation with an assumption that every process contains only one user. Public caching and authenticated data need a deliberate separation.
import { signal } from "@angular/core";
function createRequestState() { return signal<string | null>(null); }
const first = createRequestState();
const second = createRequestState();
first.set("Draft");
console.log(first(), second() ?? "none");
Draft none
Only the first state contains Draft; the other remains absent. No server request, SSR engine, or authenticated user is created.
Enable hydration only within a complete SSR contract
The contextual configuration supplies client hydration and event replay for an application that already has a supported server-rendering setup. It does not create the server entry point, route rendering policy, or hosting system. Event replay helps preserve supported interactions that occur before hydration completes, but it is not a promise that arbitrary external scripts or every event are safely reproduced. Keep the initial server and client state consistent. This fragment is deliberately non-runnable because the complete application is not supplied. A proper fixture must verify generated HTML, client attachment, and the intended interactions after hydration.
import { ApplicationConfig } from "@angular/core";
import { provideClientHydration, withEventReplay } from "@angular/platform-browser";
export const appConfig: ApplicationConfig = {
providers: [provideClientHydration(withEventReplay())]
};
In an already configured SSR application, these providers opt into the stated hydration behavior. No server rendering or browser hydration is executed here.
Preserve the structure hydration expects
Hydration requires compatible server and client DOM structure, including Angular’s relevant nodes and valid HTML. Direct DOM insertion, invalid nested markup, or a platform-dependent template branch can create mismatches. Browser globals such as window and document are not universally available during server execution. Put genuinely browser-only imperative work behind supported platform and render boundaries, and avoid rendering a different initial tree merely to hide a server error. Random identifiers, current time, locale defaults, and uncoordinated data reads can also make the two sides disagree. Pass deterministic initial data or use an explicit transfer and update policy.
Test both attachment correctness and data isolation
Use an authorized isolated SSR fixture to compare initial structure, capture hydration diagnostics, and exercise keyboard and focus behavior after attachment. Test two request contexts and verify private fields are absent from serialized public HTML or transfer data. A successful local factory test cannot prove those privacy or hydration properties. Check direct route requests, missing records, server errors, and browser-only library initialization. Avoid broad skip-hydration workarounds that conceal the real mismatch without a documented migration plan. State exactly which rendering modes were exercised and which remain illustrative; do not present a Node computation as evidence that a deployed SSR system works.
Common errors and fixes
Server execution fails on window or document access.
- Why it happens
- Browser-only work runs before a platform-appropriate boundary.
- How to fix it
- Move it to a supported browser/render lifecycle path.
- Prevent it next time
- Test server imports and rendering separately from the browser.
Hydration reports a structural mismatch.
- Why it happens
- Server HTML and client expectations diverge.
- How to fix it
- Inspect direct DOM changes, invalid markup, and initial-state differences.
- Prevent it next time
- Compare a minimal server/client fixture.
One request sees another user’s selected data.
- Why it happens
- Mutable request state was retained in a shared server owner.
- How to fix it
- Scope sensitive state and serialization to the actual request.
- Prevent it next time
- Test two synthetic request contexts together.
Check your understanding
1. Move the state signal outside createRequestState so both callers share it. What changes in the model, and why is that dangerous for request-sensitive server data?
Show worked solution
Changing the first handle also changes what the second reads because both refer to one state owner. In a long-lived server process that can mix different request data. Keep user-specific state within the actual request/application lifetime and verify isolation in an SSR fixture. A global signal is not a per-request store merely because its TypeScript type is correct.
2. A template displays a fresh random identifier during both server and browser initialization. Explain the mismatch risk and a coherent alternative.
Show worked solution
The two initial renders can produce different text or attributes, violating the expected matching structure or state. Generate a stable identifier from the domain or transfer one deliberate initial value so both sides agree, then perform any later browser-only update through a supported lifecycle. Test the actual generated HTML and hydrated view rather than only comparing TypeScript expressions.
Key takeaways
- Choose rendering mode from delivery requirements.
- Preserve compatible initial state and DOM structure.
- Keep request data isolated and browser work platform-aware.