skip to content

In Angular SSR, when do you use afterNextRender, when isPlatformBrowser with PLATFORM_ID, and when separate server and browser providers?

level: middleimportance: must knowfreq 55%

answer

  1. hook versus boolean versus DI
  2. DOM work waits for render
  3. branches not tied to rendering
  4. whole service differs by platform

basics

~20 s

Use afterNextRender for DOM work, since it runs only in the browser after rendering. Use isPlatformBrowser(PLATFORM_ID) for small non-rendering branches. When a whole service behaves differently per platform, provide separate implementations in the browser and server configs.

solid answer

~40 s

`afterNextRender()` registers a callback that the server skips and the browser runs once after the next render, optionally split into `earlyRead`, `write`, `mixedReadWrite` and `read` phases. It is the right place for measuring, focusing, or starting a DOM library. `isPlatformBrowser(inject(PLATFORM_ID))` is a synchronous boolean you can check anywhere, for logic that is not about the rendered DOM, such as not opening a socket on the server. Never use it to render different template content, because the server HTML and the browser's first render would then differ at hydration. When an entire service differs by platform, define an abstract class, provide the browser implementation in `app.config.ts` and override it in `app.config.server.ts`; the Angular docs prefer this to scattered runtime checks.

code

ts · 13 lines
ts
import { Injectable, PLATFORM_ID, inject } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';

@Injectable({ providedIn: 'root' })
export class LiveUpdates {
  private isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
  private socket: WebSocket | null = null;

  connect(url: string) {
    if (!this.isBrowser) return; // nothing to keep open while rendering on the server
    this.socket = new WebSocket(url);
  }
}

go deeper

for a junior

Know the three tools by name and that afterNextRender is where DOM work goes in an SSR app.

for a middle

Explain what each tool does on the server, the afterNextRender phases, and why a template must not branch on the platform.

for a senior

Move platform differences into providers, watch for per-process useValue state on the server, and keep runtime checks rare.

for a principal

Choose one pattern for platform differences across the codebase so reviews can spot violations instead of judging each check.

## Three tools, three jobs An Angular SSR app runs the same code on the server and in the browser. When something must behave differently, Angular gives you three tools that work at different levels: | Tool | Kind | Runs on the server | Best for | |---|---|---|---| | `afterNextRender()` | Render hook | Skipped | Reading or writing the DOM once after rendering | | `isPlatformBrowser(inject(PLATFORM_ID))` | Runtime boolean | Returns `false` | Small branches not tied to what is rendered | | Platform-specific providers | Dependency injection | The server implementation | A whole service whose behaviour differs | ## afterNextRender `afterNextRender()` registers a callback that runs **once**, in the browser, after Angular next finishes rendering. On the server it returns a no-op reference and never runs the callback. You call it in an injection context, usually a constructor, or pass an `injector` option. For layout-sensitive work you can split it into phases so reads and writes are batched: - `earlyRead`: read the DOM before writes, only when necessary; - `write`: write to the DOM; - `mixedReadWrite`: read and write together, a last resort; - `read`: read after writes. Each phase receives the previous phase's return value. `afterEveryRender()` is the same idea for every render; it was called `afterRender` before v20. Typical uses are measuring an element, moving focus, and starting a charting or map library on an element. ## isPlatformBrowser and PLATFORM_ID `PLATFORM_ID` is an injection token whose value is `'browser'` or `'server'`. `isPlatformBrowser()` and `isPlatformServer()` from `@angular/common` compare against it. The check runs immediately, wherever you call it, so it suits decisions that are not about the rendered DOM: - skip opening a WebSocket or starting a polling timer on the server; - skip calling a browser-only SDK; - choose not to read `localStorage` when there is none. The one rule: **do not use it to render different content**. An `@if (isBrowser)` in a template produces one DOM on the server and another on the browser's first render, which leads to hydration errors and layout shift. Render the same thing on both sides and change it after hydration. ## Platform-specific providers When a whole service differs, such as analytics or storage, put the difference in dependency injection: 1. Declare an abstract class as the token. 2. Provide the browser implementation in `app.config.ts`. 3. Provide the server implementation in `app.config.server.ts`. That config is merged after the shared one with `mergeApplicationConfig()`, so its provider wins on the server. ```ts import { Injectable } from '@angular/core'; export abstract class KeyValueStore { abstract get(key: string): string | null; } @Injectable() export class BrowserStore extends KeyValueStore { get(key: string) { return localStorage.getItem(key); } } @Injectable() export class ServerStore extends KeyValueStore { get() { return null; } } // app.config.ts: { provide: KeyValueStore, useClass: BrowserStore } // app.config.server.ts: { provide: KeyValueStore, useClass: ServerStore } ``` Components inject `KeyValueStore` and never ask which platform they are on. The Angular documentation recommends this over runtime checks, because the platform logic lives in one place and each implementation is testable alone. ## A server-side provider gotcha On the server, provider values written with `useValue` are evaluated **once**, when the server bundle loads, and keep their value across requests until the process restarts. A mutable object provided that way is shared by every request. When a value must be fresh per request, use `useFactory`, which runs for each rendered request. ## Testing both paths - Unit-test a platform-specific service by testing each implementation directly; neither contains a platform check. - For code that uses `isPlatformBrowser()`, provide `PLATFORM_ID` with `'server'` in a test to exercise the server branch. - `afterNextRender()` callbacks run when a component fixture renders in a browser-like test environment, so assert on their effects after rendering. - Keep at least one server-rendered test or build step, because unit tests in a browser-like environment will not catch a stray global access on the server. ## Choosing quickly - Touching the DOM or layout? Use `afterNextRender()`. - A small branch with no effect on what is rendered? Use `isPlatformBrowser()`. - A whole capability that differs? Use platform-specific providers. - About to write `@if (isBrowser)` in a template? Stop, and render the same content on both sides.

  • Why can a useValue provider in the server config leak data between requests?
    On the server, a `useValue` is evaluated once when the server bundle loads and the same value is handed to every request's injector. If it is a mutable object and one request writes to it, the next request sees that write. Use `useFactory` when a fresh value per request is needed.
  • What does each afterNextRender phase receive, and why split work into phases?
    Each phase receives the return value of the phase before it, so `earlyRead` can measure and `write` can use the measurement. Splitting reads from writes lets Angular batch them across components and avoid forcing the browser to recalculate layout between every read and write.

saying these in an interview costs you the question

  • isPlatformBrowser inside an @if is the standard way to show browser-only content.
  • afterNextRender runs on the server too, only later.
  • Platform-specific providers need a runtime check inside each implementation.
  • A useValue provider on the server is created fresh for every request.
  • afterRender is still the current name for the every-render hook.