AngularAtlased. 22.2

Ch. 12SSR & hydration55 / 75

Hydration: event replay & incremental

How the browser takes over server HTML without re-rendering it: full hydration, event replay, @defer (hydrate on …) and mismatch errors.

v22stable ~0 min read Official docs

When the page arrives rendered from the server (SSR or prerender), the browser already has the complete DOM. Without hydration, Angular would start as in a CSR application: it would throw that DOM away and build it again — a visible flicker, a broken LCP, a layout shift. Hydration means Angular walks the existing DOM, matches it against the component tree and reuses it: it binds the bindings and attaches the listeners to the nodes that already exist.

Two optimisations were built on top of it: event replay — clicks made before hydration aren’t lost — and incremental hydration — parts of the page stay static HTML, with no JS downloaded, until they’re actually needed.

In short
  • Hydration reuses the HTML sent by the server instead of recreating it, so the page does not flicker on startup.
  • Event replay remembers clicks made before JavaScript loads and replays them afterwards.
  • Incremental hydration (@defer with hydrate on …) leaves parts of the page non-interactive until they are needed. Their code is only downloaded then.
  • The HTML from the server and the one in the browser must be identical. Otherwise you get mismatch errors, with NG05xx codes.
  • ngSkipHydration is the emergency exit for a component that changes the DOM directly.
What changed in v22
  • provideClientHydration() starts incremental hydration by default, and it automatically enables event replay.
  • withIncrementalHydration() is deprecated (removal planned in v24) — remove it from the config, it’s already the default.
  • The new opt-out option is withNoIncrementalHydration().

What hydration does, step by step

On the server, after rendering, Angular annotates the HTML so it can be “found” later: the ngh attribute on each component host (an index into the hydration data), anchor comments for @if/@for/@defer, an HTML comment nghm as an integrity marker, jsaction on the elements that have listeners, and a <script id="ng-state"> with the hydration data (__nghData__) and the HTTP responses from the transfer cache.

@Component({
  selector: 'app-product-card',
  template: `
    <h2>{{ product().name }}</h2>
    @if (product().stock > 0) {
      <button (click)="addToCart()">Add to cart</button>
    }
  `,
})
export class ProductCard {
  readonly product = input.required<Product>();
  addToCart() { /* ... */ }
}

In the browser, at bootstrap, Angular no longer creates nodes: it looks for the existing ones, using the annotations. For each node it checks the type and the tag (not the text) and attaches the listeners. A different text isn’t an error — it is simply overwritten at the first change detection.

After the application becomes stable (no HTTP requests and no PendingTasks in progress), comes the final stage: Angular removes the server nodes that nobody claimed, replays the events from the queue and turns off the transfer cache. That’s why an application that doesn’t become stable has visible effects even when the DOM was reused correctly.

Without hydrationFull hydrationIncremental hydration
The server’s DOMthrown away and recreatedreusedreused
JS needed at startupall of itall of itonly what is outside the @defer (hydrate …) blocks
When a block becomes interactiveafter re-renderingat bootstrap, the whole page at onceat the block’s trigger
Clicks before the JSlostreplayed only with withEventReplay()replayed; a click on a dehydrated block hydrates it

provideClientHydration and its options

If you added SSR with ng add @angular/ssr, the provider is already in app.config.ts. With no arguments, in v22, you get the recommended set:

import { ApplicationConfig } from '@angular/core';
import { provideClientHydration } from '@angular/platform-browser';

export const appConfig: ApplicationConfig = {
  providers: [
    // v22, no arguments:
    //  ✓ non-destructive hydration (reuses the server’s DOM)
    //  ✓ HTTP transfer cache (GETs/HEADs made on the server aren’t repeated in the browser)
    //  ✓ incremental hydration (@defer with hydrate …) — includes event replay
    //  ✓ in dev mode: provideStabilityDebugging()
    provideClientHydration(),
  ],
};
// app.config.server.ts does mergeApplicationConfig(appConfig, serverConfig),
// so the provider reaches the server too — mandatory, otherwise NG0505 appears.
FunctionWhat it doesIn v22
provideClientHydration()non-destructive hydration + HTTP transfer cache + incremental hydration (with event replay)the default set
withNoIncrementalHydration()turns off incremental hydration (and, with it, event replay)new in v22
withEventReplay()event replay without incremental hydrationincluded by default; explicit only with the opt-out above
withI18nSupport()hydrates components with i18n blocks too; without it they are re-rendered from scratchoptional
withHttpTransferCacheOptions({…})configures which responses end up in the HTML: filter, includeHeaders, includePostRequests, includeRequestsWithAuthHeaders…optional
withNoHttpTransferCache()turns off the transfer cache: each GET is made twiceoptional
withIncrementalHydration()used to enable incremental hydrationdeprecated since v22 (removal planned in v24)

The provider must also exist in the server config — the app.config.server.ts generated by the CLI does mergeApplicationConfig(appConfig, serverConfig), so it’s there automatically. If it’s missing, the server doesn’t annotate the HTML, and in the browser the page is re-rendered from scratch (in dev mode, with the warning NG0505). Contradictory combinations (for example withNoHttpTransferCache() with withHttpTransferCacheOptions()) throw the error NG05001 in dev mode.

Event replay

The problem: the server’s HTML is visible long before the JS arrives (in the timeline on the SSR page that’s the yellow band). The user sees a button and presses it — but the button doesn’t have a listener yet.

The mechanism is based on JSAction, an event delegation library built into Angular:

  1. Marking — during serialisation, the server puts jsaction="click:;" on every element that has a listener in the template.
  2. Capturing — a small inline script, injected right after <body> (the event contract), listens at the body level for the event types used on the page and keeps them in memory.
  3. Replaying — after hydration, when the application becomes stable, Angular sends the queued events, in order, to the real handlers.

It works for the browser’s native events (click, mouseover, focusin, etc.). With incremental hydration, the contract stays active after hydration too, so it can catch the events in blocks that are still dehydrated.

LiveEvent replay: clicks before hydration (simulation)
magazin.ro/produse/42hydrated · interactive
Wireless headphones Aria X2
349 RON
Cart: 0Favorites: 0
main.js · executed
Event contract queue (in memory)
empty
  • Press “Reload page”, then click the buttons while the JS bar isn’t full.
Before hydration: 0 clicks · replayed 0 · lost 0
Press “Reload page”, then click a few times while the main.js bar isn’t full. With replay on, the counters “catch up” after hydration; turn it off and repeat: the clicks are lost.
Visual feedback in the hydration window

Replay saves the action, but the button doesn’t react visually until hydration. The CSS states (:hover, :active, :focus-visible) work without JS — use them, so the user knows the click “registered”.

Incremental hydration with @defer

A regular @defer renders only the @placeholder on the server: the triggers don’t run on the server. A hydrate trigger changes that: the server loads the block’s dependencies and renders the main content. In the browser the block stays dehydrated — the HTML is visible, but the JS chunk of the components in it isn’t even downloaded — until the trigger fires. Then Angular downloads the chunk, hydrates the block and replays the queued events.

Two practical consequences. First: you can put @defer above the fold without a layout shift, because the final content is already on screen, not the placeholder. Second: an event on an element with a listener in a dehydrated block triggers its hydration, whatever the declared trigger — the click isn’t lost. The exception is hydrate never: its subtree isn’t annotated at all, so there is nothing to replay.

<app-header />                                    <!-- no @defer: hydrated at bootstrap -->

@defer (hydrate on interaction) {
  <app-gallery [images]="product().images" />     <!-- the gallery’s JS: only at the first click -->
} @placeholder {
  <div class="gallery-skeleton"></div>            <!-- used only on client-side navigation -->
}

@defer (hydrate on hover) {
  <app-recommendations [productId]="id()" />
} @placeholder {
  <div class="reco-skeleton"></div>
}

@defer (hydrate on idle) {
  <app-support-chat />
} @placeholder {
  <button>Chat</button>
}

@defer (hydrate on viewport) {
  <app-reviews [productId]="id()" />              <!-- below the fold: JS when it enters the screen -->
} @placeholder {
  <div class="reviews-skeleton"></div>
}

@defer (hydrate never) {
  <app-footer />                                  <!-- static HTML forever -->
}
LiveIslands hydrated on demand (simulation)
JS downloaded: 0 KB of 218 KB · 0/6 live blocks
Headerno @defer (main.js)dehydrated48 KB, not downloaded
Galleryhydrate on interactiondehydrated36 KB, not downloaded
image 1 / 4
Recommendationshydrate on hoverdehydrated22 KB, not downloaded
Aria case · 49 RON
Support chathydrate on idledehydrated30 KB, not downloaded
Got questions?
Product description, specs, delivery…
↓ scroll down to the reviews ↓
Reviewshydrate on viewportdehydrated64 KB, not downloaded
★★★★☆ · 3 reviews shown
Footerhydrate neverstatic, no JS0 KB (would be 18)
Contact · Terms · Returns · © 2026
  • The simulation starts when the demo comes into view (or press “Reload”).
Click the gallery, hover over the recommendations, scroll down to the reviews. Watch which block hydrates, why, and how many KB it costs; then switch to “Full” and compare the JS downloaded at startup.

The hydration triggers

@defer (hydrate on idle) { … }                    <!-- requestIdleCallback -->
@defer (hydrate on idle(500)) { … }               <!-- idle, but at the latest after 500 ms -->
@defer (hydrate on viewport) { … }                <!-- the block’s content enters the screen -->
@defer (hydrate on viewport({ rootMargin: '200px' })) { … }  <!-- literal options only -->
@defer (hydrate on interaction) { … }             <!-- click / keydown in the block -->
@defer (hydrate on hover) { … }                   <!-- mouseover / focusin in the block -->
@defer (hydrate on immediate) { … }               <!-- right after the rest of the page -->
@defer (hydrate on timer(2s)) { … }               <!-- ms or s -->
@defer (hydrate when cartReady()) { … }           <!-- condition from the parent component -->
@defer (hydrate never) { … }                      <!-- never; no other hydrate triggers -->
@defer (hydrate on viewport; hydrate on timer(5s)) { … }  <!-- several = any of them (OR) -->

<!-- ✗ compile errors -->
@defer (hydrate on interaction(btn)) { … }        <!-- interaction/hover don’t take a reference -->
@defer (hydrate on viewport({ trigger: el })) { … }
@defer (hydrate never; hydrate on idle) { … }     <!-- never excludes any other hydrate trigger -->
TriggerFires whenParameters
hydrate on idlethe browser is free (requestIdleCallback)optional, a timeout: idle(500)
hydrate on viewportthe block’s content enters the screenoptional, literal IntersectionObserver options; no reference to another element
hydrate on interactionclick or keydown in the blocknone
hydrate on hovermouseover or focusin in the blocknone
hydrate on immediateas soon as the rest of the page has finished rendering—
hydrate on timer(…)after an intervalrequired, in ms or s
hydrate when exprthe expression becomes truthyworks only on the outermost dehydrated block
hydrate nevernever: static HTML, the whole subtree includedcan’t be combined with other hydrate triggers

Several triggers on the same block combine with “any” (OR). hydrate when has one limit: the expression belongs to the parent component, which must itself be hydrated for the expression to be evaluated.

Nested blocks

The component tree is hierarchical: a child can’t be active without its parent. If the trigger fires on a nested block, Angular hydrates the chain from top to bottom — first the outermost dehydrated block, then one by one down to the one that asked.

@defer (hydrate on interaction) {
  <app-product-tabs />                <!-- 1. the parent is hydrated first… -->
  @defer (hydrate on hover) {
    <app-size-guide />                <!-- 2. …then the child, even if the hover was on it -->
  }
}

Hydrate triggers in CSR and on navigation

Hydration is an optimisation of the initial load of server-rendered content. When the user reaches the same page through client-side navigation (a routerLink), or when the application has no SSR at all, the hydrate triggers are ignored: the block behaves like a regular @defer, with its @placeholder and the normal triggers. If you didn’t put any normal trigger, the compiler adds on idle.

@defer (on interaction; hydrate on viewport) {
  <app-reviews />
} @placeholder {
  <button>Show the reviews</button>
}

<!--
  Initial load, server-rendered page:
    → the server renders <app-reviews /> (not the placeholder)
    → in the browser it stays static HTML until it enters the viewport (hydrate on viewport)

  Client-side navigation (routerLink) or an application without SSR:
    → hydrate on viewport is ignored
    → the @placeholder is shown, the block loads on click (on interaction)
    → without a normal trigger, the default would have been on idle
-->
  • @placeholder remains necessary — it doesn’t show on the initial load with SSR, but it shows on any client-side navigation to the page.
  • hydrate never means “never JS on the initial load”. On a later client-side render, the block still downloads its dependencies, with the normal trigger.
  • In an application without provideClientHydration() (or with withNoIncrementalHydration()), blocks with hydrate triggers produce the warning NG0508 in dev mode.

Mismatch errors

Hydration starts from a promise: the DOM in the browser is exactly the one the server produced, including comments and spaces. Anything that breaks the promise — the browser’s HTML parser, a third-party script, a CDN, a different template branch — ends in one of three situations: an error, a warning or, the most treacherous, no error but a visible re-render.

CodeMeansTypical cause
NG0500the node found isn’t the expected one (a different tag or type) or is missinginvalid HTML, direct DOM manipulation, a third-party script
NG0501sibling nodes are missingnodes removed from outside Angular
NG0502the node isn’t found at the expected positionstructure changed after the server
NG0503projected nodes created outside AngularcreateComponent with projectableNodes from the native DOM
NG0504ngSkipHydration on an element that isn’t a component hostthe attribute put on a div
NG0505(warning) the annotations from the server are missingprovideClientHydration() is missing from the server config
NG0506(warning) the application doesn’t become stable within 10 sunfinished PendingTasks; with Zone.js, timers in the Angular zone
NG0507the HTML was modified after the servera CDN that removes the comments
LiveMismatch explorer (simulation)
Your code
<p class="promo">
  <div>Free delivery today</div>
</p>
HTML sent by the server
<p class="promo"><div>Free delivery today</div></p>
DOM in the browser, before hydration
<p class="promo"></p><div>Free delivery today</div><p></p>
error · NG0500
Console
NG0500: During hydration Angular expected <div> but the node was not found.

Angular expected this DOM:

  <p class="promo">
    <div>…</div>  <-- AT THIS LOCATION
    …
  </p>

To fix this problem:
  * check the "PromoBanner" component for hydration-related issues
  * check to see if your template has valid HTML structure
  …

What's happening: The server builds the DOM through APIs, so the invalid nesting survives until serialisation. The browser’s HTML parser, however, closes the <p> when it meets the <div>, and Angular no longer finds the expected node.

Fix: Valid HTML: <p> accepts only inline content. Use a <div class="promo"> as the parent, or a <span> inside.

Pick a pattern: see the code, the HTML sent by the server, the DOM Angular finds in the browser and the result. The console messages are quoted (shortened) from the Angular 22 sources.
Different branches on the server and in the browser

@if (isPlatformBrowser(…)) in a template doesn’t throw an error, but it’s one of the most expensive mistakes: Angular throws away the server’s DOM for the block and rebuilds it, with a layout shift. Keep the output identical on both platforms and move the browser-only initialisation into afterNextRender — or use @defer, which renders the placeholder on the server. The variants are on the Server-compatible code page.

ngSkipHydration: the emergency exit

For a component that can’t be fixed immediately (typically a library that draws directly into the DOM, like D3), the ngSkipHydration attribute on its host takes it out of hydration: the component and its children are destroyed and re-rendered, exactly as without hydration. It works only on component hosts (otherwise NG0504), and put on the root component it disables hydration for the whole application.

<!-- on the host of the component that manipulates the DOM directly (e.g. a D3 chart) -->
<app-sales-chart ngSkipHydration [data]="sales()" />

<!-- ✗ NG0504: only on component hosts, not on ordinary elements -->
<div ngSkipHydration><app-sales-chart [data]="sales()" /></div>

Debugging

  • The console, in dev mode: once the application becomes stable, a summary appears. If you don’t see it, hydration didn’t start (check NG0505).
  • Angular DevTools shows the hydration state of each component and can turn on an overlay over the hydrated parts; on a mismatch error, it highlights the guilty component.
  • View source vs Elements: View source is the server’s HTML, Elements is the current DOM. The differences between them (an extra tbody, an injected banner) are exactly the causes of NG0500.
  • Turn off JavaScript in DevTools and reload: you see exactly what the user receives before hydration.
  • NG0506: provideStabilityDebugging() lists the tasks keeping the application unstable (with Zone.js, also import zone.js/plugins/task-tracking).
  • preserveWhitespaces must have the same value in tsconfig.app.json and tsconfig.server.json; the default (false) is the recommended one.
Angular hydrated 14 component(s) and 212 node(s), 0 component(s) were skipped.
5 defer block(s) were configured to use incremental hydration.
Learn more at https://angular.dev/guide/hydration.

When YES, when NO

Use
  • Hydration: whenever you have SSR or prerender. Without it, SSR loses half its point (the DOM is rebuilt).
  • Event replay: on by default; it matters most on pages with quick actions (cart, like, menu) and slow networks.
  • hydrate on viewport for everything below the fold: reviews, similar products, a rich footer.
  • hydrate on interaction / hover for heavy widgets that sit idle until the first touch: galleries, filters, size selectors.
  • hydrate never for truly static content: an article, terms, a footer with no logic.
Avoid / the alternative
  • Applications without SSR: the hydrate triggers do nothing. Use @defer with normal triggers.
  • @defer (hydrate …) on components of a few KB: the extra chunk and request cost more than you save.
  • hydrate never on something that has listeners or state: it will never work on the initial load.
  • ngSkipHydration as a permanent “fix”: fix the cause (valid HTML, afterNextRender, DOM manipulated through Angular).
  • isPlatformBrowser branches in the template to hide problems: the alternative is @defer or afterNextRender.

Common pitfalls

Direct DOM manipulation

nativeElement.appendChild, innerHTML, document.querySelector(…).remove() in components that also run on the server produce structures that hydration can’t match. Use the template (@if, @for, bindings, Renderer2) and postpone strictly-browser code to afterNextRender.

  • Invalid HTML: a <div> inside a <p>, an <a> inside an <a>, a table without a <tbody>. Validate the template; the browser “repairs” HTML differently from how the server wrote it.
  • Different values on the server and the client (Date.now(), Math.random(), generated IDs, time zones): no error, but the text changes in front of the user. Transfer the value with TransferState.
  • Third-party scripts (analytics, consent, chat) that modify the DOM before hydration: load them afterwards, with afterNextRender.
  • withEventReplay() and withIncrementalHydration() left in “just in case” in v22: the first is redundant, the second is deprecated. A plain provideClientHydration() is enough.
  • A @placeholder removed from the blocks with hydrate, because “it isn’t visible anyway”: on client-side navigation it is.
  • Personal data in the transfer cache: responses end up in the HTML. Requests with Authorization/Cookie are excluded by default — don’t include them with includeRequestsWithAuthHeaders without a reason; use filter or transferCache: false.
  • A CDN with HTML minification that removes the comments → NG0507 on every page.

Cheat sheet

// ── Config (app.config.ts; also reaches the server through mergeApplicationConfig)
provideClientHydration();                                     // v22: DOM + transfer cache + incremental + replay
provideClientHydration(withNoIncrementalHydration(), withEventReplay());
provideClientHydration(withI18nSupport());
provideClientHydration(withHttpTransferCacheOptions({ filter, includeHeaders, includePostRequests }));
provideClientHydration(withNoHttpTransferCache());
http.get(url, { transferCache: false });                      // per-request opt-out

// ── Template
// @defer (hydrate on idle | idle(ms) | viewport | viewport({ rootMargin }) | interaction | hover | immediate | timer(2s))
// @defer (hydrate when cond)      @defer (hydrate never)      @defer (on idle; hydrate on viewport)
// <app-chart ngSkipHydration />   host: { ngSkipHydration: 'true' }   ← last resort

// ── Errors
// NG0500 different node   NG0501 sibling nodes missing   NG0502 node missing   NG0503 projected nodes created outside Angular
// NG0504 ngSkipHydration on a non-component   NG0505 annotations missing (provider missing on the server)
// NG0506 the application doesn’t become stable within 10 s   NG0507 HTML modified after the server (comments removed)
// NG0508 hydrate triggers without active incremental hydration

Ch. 12 · 0 of 3 complete

Finished this lesson?

Mark it and it shows up on your map: in the contents and on the home page.

Marks itself as done once you have read the whole lesson.

  1. SSR, prerender & render modes
  2. Hydration: event replay & incremental
  3. Server-compatible code
See your whole map

All 75 pages · type to filter

  • 01What's new in Angular 21 & 22All the important changes in v21 and v22, briefly, with a link to the page that explains each one.Start
  • 01Your first app, step by stepA reading list built from an empty component to a finished app — signals, @for, inputs and outputs, a service and a form, one step at a time.Start
  • 01Quick decisions“Where do I put this?”, “Signal or Observable?”, “Shared or feature?” — short answers to everyday questions.Start
  • 01Angular interview questions60 real interview questions for junior, mid and senior, with the answer to give, the follow-up and the red flags. Plus a mock interview.Start
  • 02Standalone & bootstrapHow an app starts without NgModule: bootstrapApplication, app.config.ts, imports in the component.Modern fundamentals
  • 02Data binding & eventsInterpolation, property and attribute bindings, events with $event and key filters, and how [( )] is built — what goes into the DOM and what comes back.Modern fundamentals
  • 02Control flow: @if, @for, @switchThe block syntax that replaces *ngIf/*ngFor/ngSwitch, with track, @empty and exhaustive checks.Modern fundamentals
  • 02@let and the new template expressionsLocal variables with @let, arrow functions, spread, regex, instanceof, comments inside tags.Modern fundamentals
  • 02@defer — lazy loading in templatesLoad heavy components only when needed: triggers, prefetch, placeholder, loading, error.Modern fundamentals
  • 02Class & style bindingsWhy [class.x] and [style.x] replaced ngClass and ngStyle (with automatic migration from v21).Modern fundamentals
  • 02Component styles & encapsulation:host, the three encapsulation modes, theming with CSS variables, and why ::ng-deep is a last resort.Modern fundamentals
  • 02Pipes: when and howPure vs impure pipes, custom pipes, and how you choose between a pipe, computed() and a method.Modern fundamentals
  • 03signal() and computed()The reactive primitives: writable state, derived state, equality, untracked and immutability rules.Signals & reactivity
  • 03effect() — when YES, when NOSide effects synchronised with signals: localStorage, logging, imperative APIs. And their traps.Signals & reactivity
  • 03linkedSignal() — editable derived stateA signal that resets when its source changes, but that you can also set manually.Signals & reactivity
  • 03resource, rxResource, httpResourceAsync data as signals: status, value, error, reload, automatic cancellation. Stable since v22.Signals & reactivity
  • 03debounced() — delayed signalsThe “debounce” version of a signal, without RxJS. Perfect for search.Signals & reactivity
  • 03Lists: search, filter, sort, paginateA chain of computed signals that recalculates only what changed, with the filters kept in the URL so a link reopens the same view.Signals & reactivity
  • 03Observables from zeroWhat an Observable is, subscribe and unsubscribe, the operators you use every week, cold vs hot — and when a signal is enough.Signals & reactivity
  • 03Signals ↔ RxJStoSignal, toObservable, takeUntilDestroyed, outputFromObservable — and when RxJS remains the right choice.Signals & reactivity
  • 04input() and output()Inputs as signals (required, transform, alias) and typed outputs. Data down, events up.Components: communication
  • 04model() — two-way bindingBanana-in-a-box [( )] with a single API. When to use it, and when input + output is better.Components: communication
  • 04viewChild & contentChildQueries as signals: viewChild, viewChildren, contentChild, contentChildren, the read option.Components: communication
  • 04Lifecycle & DestroyRefWhich hooks still matter in the signals era, afterNextRender, DestroyRef and typed ngOnChanges.Components: communication
  • 04Content projectionng-content with select, multiple slots, default content and the trap of conditional projection.Components: communication
  • 04ng-template & ngTemplateOutletTemplates as values: render props in Angular, typed context and customising components.Components: communication
  • 05Smart vs DumbContainer vs presentational: who holds the state, who only displays, and how to split them properly.Component design
  • 05Generic components <T>A correctly typed DataTable<T> or Select<T>, with templates for cells.Component design
  • 05Dynamic componentsNgComponentOutlet, createComponent with inputBinding/outputBinding, and a dashboard built from config.Component design
  • 05Directives & host compositionAttribute directives, the host: {} metadata and the Directive Composition API (hostDirectives).Component design
  • 05Custom form controlsFormValueControl (Signal Forms) vs ControlValueAccessor: a star rating, in both variants.Component design
  • 05Change detectionOnPush is the default from v22, zone.js is gone from v21. What triggers rendering and how not to get burned.Component design
  • 06inject() & @Service()The new @Service() decorator from v22, inject() vs the constructor, and the injection context.Dependency Injection
  • 06Providers & the injector hierarchyRoot vs route vs component, useClass/useValue/useFactory/useExisting, and the optional/self/skipSelf modifiers.Dependency Injection
  • 06InjectionToken & provideX()Typed configuration, provideFeature() functions, makeEnvironmentProviders and initializers.Dependency Injection
  • 06injectAsync() — lazy servicesLoad a heavy service only when you need it, with prefetch on idle.Dependency Injection
  • 07Folder structurecore / shared / features, what goes into each, and what a feature looks like inside.Application architecture
  • 07Shared vs featureWhen to move a component into shared and (especially) when NOT to. A concrete checklist.Application architecture
  • 07Interfaces & modelsInline interface or separate file? DTO vs domain model, type vs interface, where to put them.Application architecture
  • 07State management with signalsSimple signal stores, facades, local vs global state, and when you need NgRx.Application architecture
  • 07Style guide & namingThe current official conventions: file names without suffixes, protected, readonly, member order.Application architecture
  • 07Barrels, aliases & boundariesindex.ts, path aliases, a feature’s public API and dependency rules that can be checked automatically.Application architecture
  • 07Errors: @boundary & ErrorHandlerThe new @boundary block from v22.2, the global ErrorHandler, HTTP errors and errors from resource.Application architecture
  • 08Modern routing & lazy loadingloadComponent, child routes, parameters as inputs and parameter inheritance (the new default in v22).Routing
  • 08Guards & resolverscanActivate, canMatch, canDeactivate and resolve as functions, with redirects via UrlTree / RedirectCommand.Routing
  • 09Signal Forms — the basicsForms built on signals: form(), [formField], schema-based validation. Stable since v22.Forms
  • 09Signal Forms — advancedCustom and cross-field validation, async/HTTP with debounce, dynamic lists, disabled/hidden, reusable schemas.Forms
  • 09Typed Reactive FormsTyped FormGroup, nonNullable, FormArray — and how they coexist with Signal Forms.Forms
  • 10HttpClient, interceptors & the API layerHttpClient without setup (v21), Fetch by default (v22), functional interceptors and an API layer with DTO mapping.HTTP & data
  • 11Performance — a checklistWhat really matters: OnPush + signals, track, @defer, lazy routes, images, bundles and hydration.Quality & performance
  • 11Testing with VitestVitest is the default runner from v21. TestBed, components with signals, services, HTTP and harnesses.Quality & performance
  • 11DevTools: look inside your appChrome DevTools and Angular DevTools step by step: what each panel shows, with exercises that work on this site.Quality & performance
  • 11Angular AriaHeadless, accessible components that you style however you like: tabs, accordion, menu, listbox...Quality & performance
  • 12SSR, prerender & render modesWhen to render on the server, when at build time and when only in the browser: RenderMode per route, app.routes.server.ts and the real costs.SSR & hydration
  • 12Hydration: event replay & incrementalHow the browser takes over server HTML without re-rendering it: full hydration, event replay, @defer (hydrate on …) and mismatch errors.SSR & hydration
  • 12Server-compatible codewindow doesn’t exist on the server: PLATFORM_ID, afterNextRender, DOCUMENT, TransferState, the HTTP cache and PendingTasks.SSR & hydration
  • 13Micro frontendsWhen it is worth splitting an app into several deployments and when it is just complexity: Native Federation, Module Federation, iframes, Web Components.Architecture at scale
  • 13Web Components with Angular ElementscreateCustomElement: an Angular component used as an HTML tag anywhere, plus communication between apps through events and BroadcastChannel.Architecture at scale
  • 13Monorepo, libraries & boundariesA workspace with several apps and libraries: ng generate library, Nx, secondary entry points, publishing and dependency rules.Architecture at scale
  • 13Hexagonal architecture (ports & adapters)A domain without Angular, ports as abstract classes and interchangeable adapters: HTTP, localStorage or in-memory, without touching the UI.Architecture at scale
  • 14Signals under the hoodThe reactive graph: producers and consumers, push-pull, why computed is lazy and glitch-free, the diamond problem and when effects run.Advanced reactivity
  • 14Advanced RxJS: higher-order operatorsswitchMap, mergeMap, concatMap and exhaustMap on a live diagram, plus retry with backoff, shareReplay and how to avoid leaks.Advanced reactivity
  • 14NgRx SignalStoreLarge-scale state management on signals: withState, withComputed, withMethods, rxMethod, entities and hooks — and when a simple store is enough.Advanced reactivity
  • 14State machines with signalsNo more impossible states: a typed machine on signals, with transitions, guards and a live state diagram.Advanced reactivity
  • 15WebSockets: real time that survives the networkA typed socket as signals: reconnect with backoff and jitter, heartbeat, an outbox while offline, and SSR safety.Real time & offline
  • 15Optimistic updates & rollbackAn instant UI that stays honest: snapshot and rollback, out-of-order responses, idempotency keys.Real time & offline
  • 15Offline-first: Service Worker & cachingThe Angular Service Worker: caching strategies, update prompts and an outbox for changes made offline.Real time & offline
  • 16Web WorkersHeavy computation without blocking the UI: a real worker in the page, message passing and when it is not worth it.Advanced UI & performance
  • 16Huge lists: virtual scroll100,000 rows without lag: CDK virtual scroll, itemSize, trackBy and the alternatives (pagination, infinite scroll).Advanced UI & performance
  • 16Modern animations & View Transitionsanimate.enter and animate.leave instead of the deprecated @angular/animations, CSS transitions and View Transitions on navigation.Advanced UI & performance
  • 16Memory leaks: finding and fixing themSubscriptions, timers and listeners that outlive their component — how to spot them in DevTools and close them.Advanced UI & performance
  • 17Security: XSS, sanitisation & CSPWhat Angular does automatically against XSS, when bypassSecurityTrust becomes dangerous, plus CSP with nonce, Trusted Types and XSRF.Security & globalisation
  • 17Authentication: tokens & refreshAn interceptor that attaches the token, refreshes once on 401 and replays the queued requests — plus where you keep the token.Security & globalisation
  • 17OAuth 2.1 & OpenID Connect with PKCEThe authorization code flow with PKCE, step by step: verifier and challenge, state and nonce, the ID token, and why a BFF is safer.Security & globalisation
  • 17Internationalisation (i18n)$localize, the i18n attribute, ICU plural/select, a build per language vs runtime translations, and locale-based formatting.Security & globalisation