Skip to the solution
HardFrontend10 min read

How does SSR hydration work in Angular — non-destructive hydration and mismatches?

872 views

The problem

Question presented to candidate: "After enabling SSR our console shows NG0500 hydration errors on some pages. Walk me through how Angular hydration actually matches the server DOM, what counts as a mismatch, and how you would track one down."

What a strong answer should cover:

  • The server writes hydration annotations: ngh attributes, the __nghData__ map in the ng-state script, comment anchors, and the nghm integrity marker.
  • In the browser Angular walks the DOM and claims nodes, checking the element tag or node type at each position.
  • Not mismatches: different text values and different @if branches (Angular patches or re-creates them).
  • Mismatches (NG0500): invalid HTML repaired by the parser (<tr> without <tbody>), direct DOM manipulation, third-party scripts and browser extensions. Removed comments give NG0507.
  • Reading the error's expected vs actual DOM diagram and the component name it points to.
  • Fix order: valid HTML, DOM work in afterNextRender, ngSkipHydration as the last resort.

Clarifying questions expected:

  • "Does it happen on every load, or only for some users (extensions, A/B scripts)?"
  • "Is anything between the server and browser rewriting the HTML, such as a CDN optimiser?"

Code / implementation expected: Optional — reproduce one mismatch and fix it.

  • ssr
  • hydration
  • angular-18

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

10 min read · 10 sections

Read the code

AngularThree components: two harmless server and browser differences, one real mismatch (NG0500)
import { ApplicationConfig, Component, PLATFORM_ID, inject } from "@angular/core";
import { isPlatformBrowser } from "@angular/common";
import { provideClientHydration } from "@angular/platform-browser";

// SAFE: only a text value differs. Hydration reuses the <p> and updates its text.
@Component({ selector: "app-greeting", template: `<p>Rendered on the {{ where }}</p>` })
export class GreetingComponent {
  where = isPlatformBrowser(inject(PLATFORM_ID)) ? "browser" : "server";
}

// SAFE: a different @if branch. Angular drops the server branch and creates the browser one.
@Component({
  selector: "app-badge",
  template: `@if (browser) { <span>live</span> } @else { <b>static</b> }`,
})
export class BadgeComponent {
  browser = isPlatformBrowser(inject(PLATFORM_ID));
}

// UNSAFE: the browser parser fixes this table (it inserts <tbody>), so the DOM no longer
// matches what Angular rendered on the server.
@Component({
  selector: "app-prices",
  template: `<table><tr><td>Tea</td><td>2.50</td></tr></table>`,
})
export class PricesComponent {}

@Component({
  selector: "app-root",
  imports: [GreetingComponent, BadgeComponent, PricesComponent],
  template: `<app-greeting /><app-badge /><app-prices />`,
})
export class AppComponent {}

export const appConfig: ApplicationConfig = { providers: [provideClientHydration()] };
AngularThe fix: write the HTML the browser parser would produce (explicit tbody)
import { ApplicationConfig, Component, PLATFORM_ID, inject } from "@angular/core";
import { isPlatformBrowser } from "@angular/common";
import { provideClientHydration } from "@angular/platform-browser";

// SAFE: only a text value differs. Hydration reuses the <p> and updates its text.
@Component({ selector: "app-greeting", template: `<p>Rendered on the {{ where }}</p>` })
export class GreetingComponent {
  where = isPlatformBrowser(inject(PLATFORM_ID)) ? "browser" : "server";
}

// SAFE: a different @if branch. Angular drops the server branch and creates the browser one.
@Component({
  selector: "app-badge",
  template: `@if (browser) { <span>live</span> } @else { <b>static</b> }`,
})
export class BadgeComponent {
  browser = isPlatformBrowser(inject(PLATFORM_ID));
}

// UNSAFE: the browser parser fixes this table (it inserts <tbody>), so the DOM no longer
// matches what Angular rendered on the server.
@Component({
  selector: "app-prices",
  template: `<table><tbody><tr><td>Tea</td><td>2.50</td></tr></tbody></table>`,
})
export class PricesComponent {}

@Component({
  selector: "app-root",
  imports: [GreetingComponent, BadgeComponent, PricesComponent],
  template: `<app-greeting /><app-badge /><app-prices />`,
})
export class AppComponent {}

export const appConfig: ApplicationConfig = { providers: [provideClientHydration()] };

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 92 of 121 in Angular

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Angular questions
Up nextHard

What is Zone.js, how does it monkey-patch browser APIs, and how do you build a Zoneless Angular application?

Continue