diff --git a/adev/src/content/guide/ssr.md b/adev/src/content/guide/ssr.md index bd88fbd42d8..9376b6790c1 100644 --- a/adev/src/content/guide/ssr.md +++ b/adev/src/content/guide/ssr.md @@ -267,6 +267,10 @@ export class MyComponent { } ``` +NOTE: Prefer [platform-specific providers](guide/ssr#providing-platform-specific-implementations) over runtime checks with `isPlatformBrowser` or `isPlatformServer`. + +IMPORTANT: Avoid using `isPlatformBrowser` in templates with `@if` or other conditionals to render different content on server and client. This causes hydration mismatches and layout shifts, negatively impacting user experience and [Core Web Vitals](https://web.dev/learn-core-web-vitals/). Instead, use `afterNextRender` for browser-specific initialization and keep rendered content consistent across platforms. + ## Setting providers on the server On the server side, top level provider values are set once when the application code is initially parsed and evaluated. @@ -274,6 +278,73 @@ This means that providers configured with `useValue` will keep their value acros If you want to generate a new value for each request, use a factory provider with `useFactory`. The factory function will run for every incoming request, ensuring that a new value is created and assigned to the token each time. +## Providing platform-specific implementations + +When your application needs different behavior on the browser and server, provide separate service implementations for each platform. This approach centralizes platform logic in dedicated services. + +```ts +export abstract class AnalyticsService { + abstract trackEvent(name: string): void; +} +``` + +Create the browser implementation: + +```ts +@Injectable() +export class BrowserAnalyticsService implements AnalyticsService { + trackEvent(name: string): void { + // Sends the event to the browser-based third-party analytics provider + } +} +``` + +Create the server implementation: + +```ts +@Injectable() +export class ServerAnalyticsService implements AnalyticsService { + trackEvent(name: string): void { + // Records the event on the server + } +} +``` + +Register the browser implementation in your main application configuration: + +```ts +// app.config.ts +export const appConfig: ApplicationConfig = { + providers: [ + { provide: AnalyticsService, useClass: BrowserAnalyticsService }, + ] +}; +``` + +Override with the server implementation in your server configuration: + +```ts +// app.config.server.ts +const serverConfig: ApplicationConfig = { + providers: [ + { provide: AnalyticsService, useClass: ServerAnalyticsService }, + ] +}; +``` + +Inject and use the service in your components: + +```ts +@Component({/* ... */}) +export class Checkout { + private analytics = inject(AnalyticsService); + + onAction() { + this.analytics.trackEvent('action'); + } +} +``` + ## Accessing Document via DI When working with server-side rendering, you should avoid directly referencing browser-specific globals like `document`. Instead, use the [`DOCUMENT`](api/core/DOCUMENT) token to access the document object in a platform-agnostic way.