An Angular SSR app reads the user's saved theme from localStorage; how do you apply it without crashing the server render or breaking hydration?
answer
- server cannot see localStorage
- render the same default twice
- apply on html after render
- cookie lets the server know
basics
~20 sRender the same default theme on server and browser, then read localStorage only inside afterNextRender and set a class on the html element through DOCUMENT. To avoid a flash of the default, keep the choice in a cookie the server can read.
solid answer
~40 s`localStorage` does not exist in Node, so reading it in a constructor crashes the server render, and the server can never know the saved value anyway. Rendering the theme through a template branch that checks `isPlatformBrowser` makes the server HTML and the first browser render differ, which is a hydration problem. The safe pattern: render the same default on both sides; in the browser, read the value inside `afterNextRender()` (wrapped in `try`/`catch`, since storage access can throw) and toggle a class on `document.documentElement` through `inject(DOCUMENT)`. The `<html>` element sits outside the app's component tree, so hydration is untouched. That still flashes the default theme before the script runs. To remove the flash, keep the preference in a cookie so the server can render the right theme, or fall back to CSS `prefers-color-scheme`.
go deeper
Remember that localStorage does not exist on the server, so read it only in browser-only code like afterNextRender.
Explain why the server and browser must render the same default, and why the theme goes on the html element through DOCUMENT.
Recognise the flash as a separate problem, compare cookie, CSS and inline-script fixes, and note the caching risk of per-user HTML.
Decide where user preferences live, storage or cookie, based on whether the server must render them and how pages are cached.
## Why the naive version fails A theme picker saves `'dark'` or `'light'` in `localStorage`. The first implementation reads it in the root component's constructor and binds a class. With SSR this fails in three ways: 1. **Crash.** The constructor runs in Node during the server render, where `localStorage` is undefined, so the render throws. 2. **Wrong knowledge.** Even with a guard, the server cannot read the browser's storage. It never knows the saved theme. 3. **Divergence.** If the guard makes the template render different content on each side, for example `@if (isBrowser)` around a themed layout, the server HTML and the browser's first render differ, and hydration reports errors and shifts the layout. ## The safe pattern The principle is **render identically, then adjust in the browser**: - The server and the browser both render the default theme, so the DOM matches at hydration. - A browser-only callback reads the preference and applies it. - The theme is applied to `<html>` (or `<body>`), outside the components Angular hydrates, through the injected `DOCUMENT`. ```ts import { Component, DOCUMENT, afterNextRender, inject } from '@angular/core'; import { RouterOutlet } from '@angular/router'; type Theme = 'light' | 'dark'; function readSavedTheme(): Theme | null { try { const value = localStorage.getItem('theme'); return value === 'dark' || value === 'light' ? value : null; } catch { return null; // storage can be blocked by browser privacy settings } } @Component({ selector: 'app-root', imports: [RouterOutlet], template: `<router-outlet />` }) export class App { private doc = inject(DOCUMENT); constructor() { afterNextRender({ write: () => { const theme = readSavedTheme() ?? 'light'; this.doc.documentElement.classList.toggle('dark', theme === 'dark'); }, }); } } ``` The `localStorage` reference is only reached inside the `afterNextRender()` callback, which never runs on the server. ## The remaining problem: the flash The pattern above is correct but not perfect. The browser paints the server's HTML in the default theme and only switches once the application boots and renders. On a slow device that is a visible flash. Options, from most to least robust: | Approach | Server knows theme? | Flash? | Cost | |---|---|---|---| | Store the preference in a cookie and read it from the request on the server | Yes | No | Per-user HTML: must not be served from a shared cache | | Default to CSS `prefers-color-scheme` and only store overrides | Partly | Only for overrides | No server work | | A tiny inline script in `index.html` that sets the class before Angular loads | No, but applied before paint | No | Must be allowed by your Content Security Policy | | `afterNextRender()` only | No | Yes | Simplest | With the cookie approach, the server renders the correct class from the start, so the browser's first render matches and nothing moves. Reading request data on the server uses Angular's request tokens and a server-rendered route, which belong to route-level SSR configuration. ## Traps in this scenario - **Binding the theme in the root template from a signal set in the constructor.** If the signal is set only in the browser before the first render, bound values differ from the server HTML. Angular updates the bindings, so the user sees a switch; if it changes structure, hydration errors follow. - **Reading storage in a service field initializer.** Field initializers run at construction on the server just like constructors. - **Forgetting that storage can throw.** Browser privacy settings can make storage access throw a security error; wrap the read. - **Caching the HTML.** If the server renders per-user theme classes from a cookie, a shared cache must not store that page for everyone. ## Testing it - A server-rendered test (or a prerender of the route in CI) proves the constructor path no longer touches storage. - A browser test sets `localStorage` before the app boots and checks that `<html>` gains the `dark` class after the first render. - A test with storage access throwing checks that the app still renders with the default theme. ## What a strong answer shows It separates the three failure modes (crash, missing knowledge, divergence), applies the theme outside the hydrated tree through `DOCUMENT` in `afterNextRender()`, and recognises the flash as a separate problem with its own trade-offs.
- Why apply the theme to the html element rather than to a component's host?`<html>` sits outside the component tree Angular hydrates, so changing its classes cannot conflict with hydration's view of the DOM. It also lets one class drive CSS custom properties for the whole page, including content outside the app root.
- If the preference moves to a cookie, what new risk appears?The server now renders different HTML per user, so any shared cache in front of the app could store one visitor's themed page and serve it to others. Mark those responses as private, or vary the cache on the cookie, and keep the default theme for anonymous cached pages.
It is like a hotel that prepares every room with standard white bedding because it cannot see guests' preferences at the door; once the guest arrives and says what they like, housekeeping changes the sheets. A note sent ahead (a cookie) lets the room be ready from the start.
saying these in an interview costs you the question
- Guarding localStorage with isPlatformBrowser removes the flash of the default theme.
- The server can read localStorage if the request comes from the same browser.
- Rendering the themed layout inside @if (isBrowser) is safe for hydration.
- Storage reads never throw, so no try/catch is needed.
- Changing classes on html interferes with component hydration.