Skip to the solution
MediumFrontend9 min read

How do you handle SSR hydration mismatch errors using ngSkipHydration?

1.1k views

The problem

Question presented to candidate: "We wrap an old jQuery charting plugin in an Angular component. Since turning on SSR it throws NG0500 during hydration and the page stops working. How do you use ngSkipHydration to fix this, and what does it cost?"

What a strong answer should cover:

  • Why the plugin breaks hydration: it changes the DOM outside Angular, so the browser DOM no longer matches the server map (NG0500).
  • ngSkipHydration on the component host (template attribute or host metadata) skips hydration for that component and its whole subtree.
  • What happens instead: server HTML is shown, then that subtree is discarded and re-rendered in the browser.
  • Limits: only on component hosts (NG0504 otherwise), static attribute only (not a binding), and its presence is what counts.
  • Costs: no node reuse, no event replay inside it; possible flicker for that island.
  • Better fixes to try first: move plugin init to afterNextRender, fix invalid HTML, use @defer for browser-only widgets.

Clarifying questions expected:

  • "Does the plugin need to run on the server at all, or only in the browser?"
  • "Is it one component, or several places using the same widget?"

Code / implementation expected: Yes β€” a component with ngSkipHydration, ideally both the template and the host metadata form.

  • ssr
  • hydration
  • troubleshooting

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

9 min read Β· 9 sections

Read the code

AngularOpting two legacy DOM-writing widgets out of hydration: in the template and in host metadata
import { ApplicationConfig, Component, ElementRef, inject, signal } from "@angular/core";
import { provideClientHydration } from "@angular/platform-browser";

// Stands in for a jQuery or charting plugin: it writes DOM behind Angular back,
// on the server AND again in the browser.
function legacyPlugin(host: HTMLElement) {
  const banner = host.ownerDocument.createElement("div");
  banner.textContent = "plugin banner";
  host.insertBefore(banner, host.firstChild);
}

@Component({ selector: "app-legacy-chart", template: `<p>chart body</p>` })
export class LegacyChartComponent {
  constructor() { legacyPlugin(inject(ElementRef).nativeElement); }
}

// Same problem, but the component opts itself out, so no caller can forget.
@Component({
  selector: "app-legacy-map",
  template: `<p>map body</p>`,
  host: { ngSkipHydration: "true" },
})
export class LegacyMapComponent {
  constructor() { legacyPlugin(inject(ElementRef).nativeElement); }
}

@Component({
  selector: "app-root",
  imports: [LegacyChartComponent, LegacyMapComponent],
  template: `
    <h1>{{ title() }}</h1>
    <app-legacy-chart ngSkipHydration />
    <app-legacy-map />
    <button (click)="title.set('Dashboard (clicked)')">Click</button>
  `,
})
export class AppComponent {
  title = signal("Dashboard");
}

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 17 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 nextMedium

What are route guards in Angular?

Continue