NEURALNG
FOUNDATION

SSR & Hydration

Render useful HTML on the server, reuse it in the browser and keep component identity, state and interaction deterministic across the boundary.

Server safeEvent replay readyStable DOM identity

RENDERING STRATEGY

Choose a mode per route

Angular supports server rendering per request, build-time prerendering and client rendering. NeuralNg does not force one mode; the same component contracts remain valid in each.

Per request

SSR

Personalized or frequently changing public content.

At build time

SSG

Documentation, marketing and other cacheable routes.

In the browser

CSR

Private tools where initial HTML and SEO are not priorities.

app.routes.server.ts TypeScript
import { RenderMode, type ServerRoute } from '@angular/ssr';

export const serverRoutes: ServerRoute[] = [
  { path: '', renderMode: RenderMode.Prerender },
  { path: 'docs/**', renderMode: RenderMode.Prerender },
  { path: 'account/**', renderMode: RenderMode.Server },
  { path: 'admin/**', renderMode: RenderMode.Client },
];

Enable hydration once at application level

provideClientHydration() lets Angular reconcile and reuse server-rendered DOM. Event replay captures supported interactions that occur before hydration finishes. Angular 22 enables incremental hydration through this provider by default.

app.config.ts TypeScript
import { ApplicationConfig } from '@angular/core';
import {
  provideClientHydration,
  withEventReplay,
} from '@angular/platform-browser';
import { provideNeuralNg } from '@neural-ng/core';

export const appConfig: ApplicationConfig = {
  providers: [
    provideClientHydration(withEventReplay()),
    provideNeuralNg({ direction: 'auto' }),
  ],
};
app.config.server.ts TypeScript
import { ApplicationConfig, mergeApplicationConfig } from '@angular/core';
import { provideServerRendering, withRoutes } from '@angular/ssr';
import { appConfig } from './app.config';
import { serverRoutes } from './app.routes.server';

const serverConfig: ApplicationConfig = {
  providers: [provideServerRendering(withRoutes(serverRoutes))],
};

export const config = mergeApplicationConfig(appConfig, serverConfig);

What NeuralNg guarantees

Components render meaningful closed or inactive server markup without requiring browser globals. Browser-only positioning, focus, storage and top-layer work starts after the browser takes ownership.

Deterministic identity

APP_ID-scoped generators and explicit ID inputs preserve ARIA relationships.

Browser guards

DOM measurements, observers and global listeners do not execute on the server.

Closed overlays

Popup positioning and focus work begin only after a client-side open action.

Lifecycle cleanup

Observers, listeners, top-layer state and focus ownership are released on destroy.

Server and browser must render the same tree

The initial locale, direction, collection order, selected values, open state and conditional branches must agree. Prefer explicit public IDs for important forms and composite widgets; generated IDs are deterministic only while component creation order remains identical.

stable-identity.html HTML
<neural-field controlId="checkout-email">
  <label neuralFieldLabel>Email</label>
  <input neuralInput type="email" />
  <small neuralFieldHint>Receipt delivery address.</small>
</neural-field>

<neural-tabs tabsId="account-tabs" [(value)]="activeTab">
  ...
</neural-tabs>

Common mismatch sources

  • Random IDs or current time in templates
  • Viewport-dependent initial branches
  • Browser storage read during rendering
  • Different server and client locale
  • Invalid HTML corrected by the browser
  • Direct DOM insertion before hydration
browser-state.ts TypeScript
// Avoid during initial rendering:
readonly id = Math.random().toString(36);
readonly now = new Date();
readonly compact = window.innerWidth < 768;
readonly mode = localStorage.getItem('mode');

// Prefer stable inputs and browser-only enhancement:
readonly id = input.required<string>();
readonly initialTimestamp = input.required<string>();

afterNextRender(() => {
  this.restoreBrowserPreferences();
});

Defer browser-only enhancement

Use Angular render callbacks for measurement, focus and third-party browser APIs. Use isPlatformBrowser in reusable services that must expose a safe server no-op. Do not scatter eager window and document reads through component constructors.

chart-panel.ts TypeScript
import { Component, ElementRef, afterNextRender, viewChild } from '@angular/core';

@Component({ /* ... */ })
export class ChartPanel {
  readonly canvas = viewChild.required<ElementRef<HTMLCanvasElement>>('canvas');

  constructor() {
    afterNextRender(() => {
      // Runs in the browser after Angular has rendered.
      const width = this.canvas().nativeElement.getBoundingClientRect().width;
      this.initializeChart(width);
    });
  }
}

Overlay markup starts stable and closed

Select, MultiSelect, AutoComplete, TreeSelect, Popover, Tooltip, Dialog and Drawer postpone top-layer positioning and focus work until the browser. An appendTo="body" panel is moved only after opening, never while server HTML is being produced.

overlays.html HTML
<neural-select
  controlId="shipping-country"
  [options]="countries"
  appendTo="body"
/>

<neural-dialog #review ariaLabel="Review order">
  ...
</neural-dialog>
Do not server-render a modal as initially open unless the same state, content and focus intent are guaranteed on the client. A closed initial state is the safest default for reusable overlays.

Transfer initial data instead of requesting it twice

Angular hydration includes HTTP transfer caching for eligible server requests. Keep the initial result serializable, preserve collection order and avoid emitting a temporary empty state in the browser before transferred data resolves.

Serialize

Dates, Maps and class instances need an explicit wire representation.

Authorize

Never expose server-only credentials or private provider state in HTML.

Revalidate

Treat transferred data as the initial snapshot, then refresh through application policy.

Hydrate expensive regions when they become useful

Incremental hydration combines server-rendered @defer content with hydrate triggers. Reserve it for meaningful boundaries; splitting every small control increases complexity without improving the user journey.

report.html HTML
@defer (on viewport; hydrate on interaction) {
  <app-heavy-report />
} @placeholder {
  <neural-skeleton width="100%" height="18rem" />
}

The server-rendered block and its placeholder must reserve compatible space. Event replay is automatically available with Angular's incremental hydration.

Diagnose the production path

A client-only development server cannot prove SSR safety. Build the production target, inspect the returned HTML with JavaScript disabled, then use Angular DevTools and the browser console to locate hydrated nodes and mismatches.

verification.sh Bash
// Production-equivalent verification
npx nx build neural-site --configuration=production

// Then inspect the served HTML before JavaScript executes:
// 1. Content and accessible names are present.
// 2. Angular reports hydrated nodes without mismatch errors.
// 3. Event replay preserves early clicks.
// 4. No overlay, timer or global listener leaks after navigation.

Deployment checklist

Every public route produces useful HTML before JavaScript.

Server and browser start with identical locale, direction and form state.

No initial branch depends on time, randomness, viewport or browser storage.

Explicit IDs are used for critical label, hint and overlay relationships.

Early interactions survive hydration and do not execute twice.

Overlays restore focus and leave no document listeners after navigation.

Hydration is tested in the deployed runtime, not only the CSR dev server.