AngularAtlased. 22.2

Cap. 12SSR & hidratare56 / 75

Cod compatibil cu serverul

window nu există pe server: PLATFORM_ID, afterNextRender, DOCUMENT, TransferState, cache-ul HTTP și PendingTasks.

v22 ~0 min de citit Documentația oficială

Cu SSR sau prerender, componentele tale rulează de două ori: o dată în Node — la fiecare request sau la build — ca să producă HTML, și încă o dată în browser, la hidratare. Constructorii, field initializer-ele, ngOnInit, effect(), serviciile, interceptorii și guard-urile rulează pe ambele platforme. Doar câteva API-uri sunt rezervate browserului: afterNextRender, afterEveryRender, afterRenderEffect și, evident, evenimentele utilizatorului.

Pe server nu există window, localStorage, layout sau observer-e. Există un document — emulat de Domino — dar numai prin inject(DOCUMENT). Codul scris „pentru browser” merge perfect în ng serve fără SSR și cade abia în build-ul cu server. Sau, mai rău, nu cade, dar trimite HTML greșit: temă greșită, date lipsă, fiecare request făcut de două ori.

Pe scurt
  • Pe server nu există window, document sau localStorage. Codul care are nevoie de ele rulează în afterNextRender.
  • Ce ai încărcat pe server ajunge în browser prin cache-ul HTTP de transfer, așa că HttpClient nu repetă request-ul la hidratare.
  • Serverul trimite pagina abia când aplicația nu mai are task-uri în așteptare (PendingTasks). Un timer care nu se oprește ține request-ul deschis.
  • O pagină inexistentă trebuie să răspundă cu statusul 404, nu doar să afișeze „negăsit”.
  • Variabilele la nivel de modul sunt comune tuturor request-urilor de pe server. Starea unui utilizator stă în servicii, nu acolo.
De ce contează
Pagina asta e lista de verificare pentru code review într-un proiect cu SSR: ce cade pe server, unde muți codul de browser, cum nu faci munca de două ori (TransferState și cache-ul HTTP), când decide serverul că pagina e gata (PendingTasks) și cum eviți scurgerile de stare între utilizatori. Configurarea SSR e în SSR, prerender & render modes, iar preluarea HTML-ului în browser în Hidratare.

Ce nu există pe server

Serverul Angular rulează în Node, cu un DOM emulat de Domino (inclus în @angular/platform-server). Distincția importantă: globalele de browser lipsesc complet și dau ReferenceError, iar documentul primit prin DI are structură, dar nu are layout. Tabelul e verificat pe Node 22/24 și pe Domino-ul din @angular/platform-server 22.2.2.

APIPe serverCe folosești în schimb
window, document (global), location, historynu există: ReferenceError: window is not definedinject(DOCUMENT), Router, Location
localStorage, sessionStoragenu existăafterNextRender; pentru ce contează la randare, un cookie citit din REQUEST
matchMedia, innerWidthlipsesc și de pe document.defaultView (fereastra Domino)CSS media queries; BreakpointObserver din CDK (pe server, query-urile nu se potrivesc); afterNextRender
IntersectionObserver, ResizeObserver, MutationObserver, requestAnimationFramenu există@defer (on viewport); afterNextRender + DestroyRef
getBoundingClientRect(), offsetWidth, scrollHeightelementele Domino nu le au (TypeError sau undefined)afterNextRender({ read })
document.cookiegetter-ul aruncă Error: NotYetImplementedinject(REQUEST)?.headers.get('cookie')
navigatorexistă în Node 21+, dar e al serverului (userAgent: 'Node.js/22')header-ele din REQUEST; isPlatformBrowser
setTimeout, setInterval, fetchexistă, dar timerele nu se opresc singure, iar fetch nu acceptă URL-uri relativeafterNextRender + DestroyRef; HttpClient

Ce rulează unde

CodServerBrowser
field initializers, constructor, ngOnInit, ngOnChangesdada
effect(), computed(), template-ul, pipe-uriledada
servicii, interceptori, guards, resolversda — instanțe noi la fiecare requestda
ngOnDestroy, DestroyRef.onDestroyda — serverul distruge aplicația după randareda
afterNextRender, afterEveryRender, afterRenderEffectnu (NOOP)da
event listeners: (click), (input)…nu — nimeni nu dă click pe serverda
@defer fără hydratedoar @placeholdertrigger-ele pornesc aici
LiveAr rula pe server? (simulare a mediului de server)
6 erori2 atenție3 OK
theme = 'light';

constructor() {
  this.theme = localStorage.getItem('theme') ?? 'light';
}
Eroare · server (Node + Domino)ReferenceError: localStorage is not defined

De ce: Constructorul rulează și pe server, iar în Node nu există localStorage. Chiar dacă ar exista, serverul n-ar avea acces la storage-ul utilizatorului.

Remedierea
readonly theme = signal('light');
constructor() {
  afterNextRender(() =>
    this.theme.set(localStorage.getItem('theme') ?? 'light'));
}
// sau: cookie citit pe server din inject(REQUEST)
Comută între Browser și „Rulează pe server”. Fiecare fragment chiar se execută: pe „server”, într-un sandbox în care globalele de browser lipsesc ca în Node, navigator e cel din Node, iar inject(DOCUMENT) dă un document de tip Domino. Mesajele de eroare sunt aceleași ca în Node. Doar modificatorii TypeScript (private, readonly) sunt eliminați înainte de execuție.
Regula
Niciun API de browser în constructor, field initializers, ngOnInit sau effect(). Tot ce atinge DOM-ul, layout-ul, storage-ul sau o bibliotecă de browser merge în afterNextRender / afterRenderEffect — sau în spatele unui serviciu cu implementare separată pentru server.

afterNextRender și fazele

afterNextRender și afterEveryRender (stabile din v20, când afterRender a fost redenumit afterEveryRender) înregistrează callback-uri care rulează după ce Angular a terminat de randat — doar în browser. Pe server sunt NOOP: callback-ul nici măcar nu se înregistrează. De aceea sunt locul implicit pentru orice cod care atinge DOM-ul, layout-ul sau API-urile de fereastră. Trebuie apelate într-un context de injecție (constructor, field initializer) sau cu opțiunea injector, și se curăță singure la distrugerea componentei.

Le poți da o funcție sau un obiect cu faze. Fazele rulează în ordinea earlyRead → write → mixedReadWrite → read, iar fiecare primește valoarea întoarsă de faza anterioară. Separarea există ca Angular să grupeze citirile și scrierile din toată aplicația: o citire de layout după o scriere obligă browserul să recalculeze layout-ul pe loc (layout thrashing). O funcție simplă rulează în mixedReadWrite, faza pe care documentația te roagă s-o eviți.

FazaAi voieExemplu
earlyReaddoar citiri, înaintea scrierilormăsori ceva de care depinde o scriere (layout custom)
writedoar scrieristiluri, inițializarea unei biblioteci
mixedReadWritecitiri și scrieri amestecatedoar dacă munca nu se poate împărți
readdoar citiri, după scrierimăsori rezultatul final și îl pui într-un semnal
import { Component, ElementRef, afterNextRender, input, viewChild } from '@angular/core';

/** Micșorează titlul până încape pe un rând. Pe server nu se întâmplă nimic. */
@Component({
  selector: 'app-fit-title',
  template: `<h2 #title class="fit">{{ text() }}</h2>`,
})
export class FitTitle {
  readonly text = input.required<string>();
  private readonly title = viewChild.required<ElementRef<HTMLElement>>('title');

  constructor() {
    // O singură dată, după prima randare, DOAR în browser (pe server e NOOP).
    afterNextRender({
      // 1. earlyRead: citește layout-ul înaintea oricărei scrieri
      earlyRead: () => {
        const el = this.title().nativeElement;
        return Math.min(1, el.clientWidth / el.scrollWidth); // 1 = încape
      },
      // 2. write: primește rezultatul fazei anterioare și doar scrie
      write: (ratio) => {
        this.title().nativeElement.style.fontSize = `${Math.floor(32 * ratio)}px`;
      },
    });
  }
}
LivePLATFORM_ID și momentul măsurării (API real)
ExpresieAici (live) Pe server ar fi
inject(PLATFORM_ID)'server''server'
isPlatformBrowser(id)falsefalse
isPlatformServer(id)truetrue
inject(REQUEST)nullRequest (url, headers, cookie)
inject(RESPONSE_INIT)null{ status, headers }
Componenta măsuratălățime acum (afterEveryRender, faza read): 0 px
  1. +0.1 msconstructorisConnected=false, lățime=0px, copii=0, binding=null
  2. +0.2 msngOnInitisConnected=true, lățime=0px, copii=0, binding=""

Pe server ar rula doar rândurile constructor și ngOnInit (fără layout: lățimile n-ar exista); afterNextRender și afterEveryRender sunt ignorate.

Tabelul arată valorile reale din această pagină. Apasă „Remontează componenta” și citește log-ul: în constructor elementul nu e în pagină și nu are conținut; în ngOnInit e atașat, dar binding-urile din template încă nu sunt aplicate (și pe server n-ar avea layout); abia în fazele afterNextRender totul e randat. Mută slider-ul: afterEveryRender remăsoară după fiecare randare.

afterEveryRender sau afterRenderEffect

afterEveryRender rulează după fiecare randare a aplicației, inclusiv când s-a schimbat ceva într-o cu totul altă componentă. Dacă munca ta depinde de niște semnale, afterRenderEffect e varianta mai ieftină: un effect care rulează după randare doar când dependențele lui s-au schimbat (și tot doar în browser). Diferența față de effect(): effect rulează și pe server și nu are faze de citire/scriere.

import { Component, ElementRef, afterRenderEffect, input, viewChild } from '@angular/core';

@Component({
  selector: 'app-chat-log',
  template: `
    <div #scroller class="messages">
      @for (m of messages(); track m.id) { <p>{{ m.text }}</p> }
    </div>
  `,
})
export class ChatLog {
  readonly messages = input.required<readonly { id: number; text: string }[]>();
  private readonly scroller = viewChild.required<ElementRef<HTMLElement>>('scroller');

  constructor() {
    // ✗ ar rula după FIECARE randare a aplicației, chiar dacă lista nu s-a schimbat:
    // afterEveryRender({ write: () => this.scrollToBottom() });

    // ✓ rulează după randare doar când semnalele citite s-au schimbat (tot doar în browser)
    afterRenderEffect({
      write: () => {
        this.messages(); // dependență
        this.scrollToBottom();
      },
    });
  }

  private scrollToBottom(): void {
    const el = this.scroller().nativeElement;
    el.scrollTop = el.scrollHeight;
  }
}
Hidratarea nu e garantată
Documentația API e explicită: componentele nu sunt garantat hidratate când rulează un callback afterNextRender. Citește și scrie DOM-ul cu grijă și nu muta noduri pe care le randează Angular — altfel apar erorile de nepotrivire descrise în Hidratare.

Biblioteci care ating window

Multe biblioteci (hărți, grafice, editoare rich-text) accesează window chiar la import. Serverul cade când încarcă modulul, înainte să ruleze vreo componentă, și nicio verificare din constructor nu te mai salvează. Soluția e import() dinamic în afterNextRender; bonus, biblioteca ajunge într-un chunk separat. Pentru componente întregi, @defer face același lucru declarativ.

import { Component, DestroyRef, ElementRef, afterNextRender, inject, viewChild } from '@angular/core';
// ✗ import * as L from 'leaflet';  — modulul atinge window încă de la import:
//    serverul cade când încarcă fișierul, înainte să ruleze vreo componentă.

@Component({
  selector: 'app-store-map',
  template: `<div #host class="map" aria-label="Harta magazinelor"></div>`,
  // înălțimea fixă rezervă spațiul și în HTML-ul de pe server → fără layout shift
  styles: `.map { height: 320px; }`,
})
export class StoreMap {
  private readonly host = viewChild.required<ElementRef<HTMLElement>>('host');
  private readonly destroyRef = inject(DestroyRef); // inject() doar sincron, nu după await

  constructor() {
    afterNextRender(async () => {
      const L = await import('leaflet'); // chunk separat, descărcat doar în browser
      if (this.destroyRef.destroyed) return; // utilizatorul a plecat între timp
      const map = L.map(this.host().nativeElement).setView([44.4268, 26.1025], 12);
      this.destroyRef.onDestroy(() => map.remove());
    });
  }
}

Dacă biblioteca rescrie DOM-ul pe care l-a randat Angular, hidratarea se plânge de nepotriviri. ngSkipHydration pe componenta-gazdă oprește hidratarea doar pentru ea — e un plasture, nu o soluție.

DOCUMENT, PLATFORM_ID și provideri pe platformă

inject(DOCUMENT) — din @angular/core începând cu v20; @angular/common îl reexportă — îți dă documentul curent pe orice platformă. Pe server e documentul Domino al request-ului: poți citi și scrie structura (title, <meta>, <link rel="canonical">, atribute pe <html>), iar modificările ajung în HTML-ul trimis. Nu ai layout, cookie-uri sau matchMedia.

import { DOCUMENT, Service, inject } from '@angular/core';

@Service()
export class CanonicalLink {
  private readonly document = inject(DOCUMENT); // pe server: documentul Domino al request-ului

  /** Rulează și pe server: tag-ul ajunge în HTML-ul văzut de crawlere. */
  set(url: string): void {
    let link = this.document.head.querySelector<HTMLLinkElement>('link[rel="canonical"]');
    if (!link) {
      link = this.document.createElement('link');
      link.rel = 'canonical';
      this.document.head.appendChild(link);
    }
    link.href = url;
  }
}

// Pentru <title> și <meta> există deja servicii SSR-safe în @angular/platform-browser:
//   inject(Title).setTitle('Telefon X — Magazin');
//   inject(Meta).updateTag({ name: 'description', content: '…' });

inject(PLATFORM_ID) întoarce 'browser' sau 'server' și îl testezi cu isPlatformBrowser / isPlatformServer din @angular/common. Merge, dar ghidul oficial recomandă providerii pe platformă în locul verificărilor la runtime, din două motive: verificările se înmulțesc prin tot codul, iar folosite într-un @if din template produc conținut diferit pe server și în browser, adică layout shift și erori de hidratare. Ține-l pentru efecte secundare care nu schimbă ce se randează.

import { isPlatformBrowser } from '@angular/common';
import { PLATFORM_ID, Service, inject } from '@angular/core';

@Service()
export class Analytics {
  // PLATFORM_ID e 'browser' sau 'server'; nu-l compara manual, folosește helper-ele din @angular/common
  private readonly isBrowser = isPlatformBrowser(inject(PLATFORM_ID));

  track(event: string): void {
    if (!this.isBrowser) return; // ok: un efect secundar care nu schimbă ce se randează
    navigator.sendBeacon('/collect', JSON.stringify({ event, at: Date.now() }));
  }
}

// ✗ NU așa — server și browser randează conținut diferit → layout shift și erori de hidratare:
//   @if (isBrowser) { <app-recommendations /> } @else { <p>Se încarcă…</p> }
// ✓ folosește @defer (on viewport) sau afterNextRender pentru ce e doar de browser.

Când platformele au nevoie de comportament diferit, varianta curată e un contract cu două implementări: componentele injectează clasa abstractă, app.config.ts dă implementarea de browser, iar app.config.server.ts o suprascrie. Niciun if în codul de business — același principiu ca în Providers și Injection tokens.

import { Injectable } from '@angular/core';

// Contractul: componentele depind doar de clasa abstractă.
export abstract class AnalyticsService {
  abstract trackEvent(name: string): void;
}

@Injectable()
export class BrowserAnalyticsService implements AnalyticsService {
  trackEvent(name: string): void {
    navigator.sendBeacon('/collect', name); // API de browser — aici e voie
  }
}

@Injectable()
export class ServerAnalyticsService implements AnalyticsService {
  trackEvent(_name: string): void {
    // pe server: nimic (sau un log), fără API-uri de browser
  }
}

TransferState

Fără nimic special, o pagină SSR își încarcă datele de două ori: o dată pe server, ca să randeze HTML-ul, și încă o dată în browser, pentru că la hidratare componentele pornesc de la zero, cu starea goală. Rezultatul: un request în plus și conținutul de pe server înlocuit pentru o clipă de un skeleton.

TransferState e un dicționar cheie → valoare care călătorește în HTML. Pe server scrii în el; înainte de serializare Angular apelează toJson() și pune rezultatul într-un <script id="ng-state" type="application/json"> la finalul lui body — doar dacă nu e gol. În browser, la prima injectare, TransferState citește scriptul înapoi. Cheile se creează cu makeStateKey<T>('…'), care leagă și tipul valorii. API-ul complet: get(key, default), set, remove, hasKey, isEmpty, onSerialize, toJson.

import { isPlatformServer } from '@angular/common';
import { PLATFORM_ID, Service, TransferState, inject, makeStateKey } from '@angular/core';

export interface Article { slug: string; title: string; body: string }

// Cheie tipizată: TransferState știe că sub ea stă un Article
const articleKey = (slug: string) => makeStateKey<Article>(`article:${slug}`);

@Service()
export class ArticleStore {
  private readonly cms = inject(CmsClient); // SDK cu fetch propriu → NU trece prin HttpClient
  private readonly state = inject(TransferState);
  private readonly isServer = isPlatformServer(inject(PLATFORM_ID));

  async load(slug: string): Promise<Article> {
    const key = articleKey(slug);
    const cached = this.state.get(key, null);
    if (cached) {
      this.state.remove(key); // folosită o singură dată: navigările ulterioare cer date proaspete
      return cached;
    }
    const article = await this.cms.getArticle(slug);
    if (this.isServer) this.state.set(key, article); // ajunge în <script id="ng-state">
    return article;
  }
}
LiveTransferState: o cerere în loc de două (API real)

HTML trimis de server

Nimic încă. Pornește randarea pe server.

Ce vede utilizatorul

Pagina nu s-a încărcat încă.
0request-uri către /api/products/3
Pasul 1 face un GET real (prin backend-ul simulat) și scrie în TransferState; JSON-ul din script e chiar rezultatul lui toJson(). Pasul 2 creează un TransferState nou dintr-un document care conține acel script, exact cum face browserul la bootstrap. Debifează „folosește TransferState” și repetă: apare al doilea request și skeleton-ul.

Valorile trec prin JSON.stringify / JSON.parse: un Date ajunge string, Map și Set devin {}, instanțele de clasă își pierd metodele. Transferă DTO-uri simple (vezi Interfețe & modele). Tot ce pui acolo e vizibil în sursa paginii și îngreunează HTML-ul: demo-ul îți arată câte caractere adaugă.

Pentru HttpClient nu scrii codul ăsta
GET-urile prin HttpClient sunt transferate automat (secțiunea următoare). TransferState manual e pentru date care nu trec prin HttpClient: SDK-uri (Firebase, un CMS), fetch direct, calcule scumpe făcute o dată pe server. Pentru resurse, din v22 există și resource({ id }).

Cache-ul HTTP de transfer

provideClientHydration() pornește implicit un interceptor care aplică exact pattern-ul de mai sus pentru HttpClient. Pe server salvează răspunsurile în TransferState, sub o cheie calculată ca hash SHA-256 din metodă, tipul răspunsului, URL, body și parametri. În browser le servește din cache — dar doar până când aplicația devine stabilă prima dată; după aceea totul merge la rețea, ca de obicei. httpResource trece prin HttpClient, deci beneficiază automat, la fel și interceptorii tăi (vezi HTTP).

În 22.2.2 intră implicit în cache doar request-urile GET și HEAD care:

  • nu au header-e Authorization, Proxy-Authorization sau Cookie;
  • nu sunt trimise cu withCredentials sau cu credentials: 'include' / 'same-origin';
  • nu au Cache-Control cu no-store, no-cache sau private (nici request-ul, nici răspunsul) și nu folosesc cache: 'no-store' / 'no-cache';
  • al căror răspuns nu conține Set-Cookie.
import { ApplicationConfig } from '@angular/core';
import { provideClientHydration, withHttpTransferCacheOptions } from '@angular/platform-browser';

export const appConfig: ApplicationConfig = {
  providers: [
    // Cache-ul de transfer e PORNIT implicit de provideClientHydration();
    // withHttpTransferCacheOptions doar îl reglează.
    provideClientHydration(
      withHttpTransferCacheOptions({
        // ce header-e ale răspunsului se salvează (implicit: niciunul)
        includeHeaders: ['ETag'],
        // false → requestul nu intră în cache (date per utilizator, date care se schimbă des)
        filter: (req) => !req.url.includes('/api/cart'),
        // POST-uri de citire (ex. GraphQL) — doar dacă sunt idempotente
        includePostRequests: true,
        // implicit false: request-urile cu Authorization / Proxy-Authorization / Cookie sunt sărite
        includeRequestsWithAuthHeaders: false,
      }),
    ),
  ],
};
OpțiuneImplicitCând o schimbi
filter(req)toate cererile eligibilescoți endpoint-uri per utilizator sau foarte volatile (false = nu se cache-uiește)
includeHeadersniciun headerbrowserul are nevoie de ETag, Link… Niciodată header-e sensibile
includePostRequestsfalsePOST folosit pentru citiri (GraphQL), doar dacă e idempotent
includeRequestsWithAuthHeadersfalseheader-ul de auth nu schimbă răspunsul (o cheie publică)
includeRequestsWithCredentialsfalserăspunsurile cu credentials sunt sigur publice
includeNonCacheableRequestsfalseignori conștient Cache-Control / Set-Cookie (rar)

Resurse care nu folosesc HttpClient v22

Din v22, resource() și rxResource() acceptă id. Valoarea rezolvată pe server e scrisă în TransferState sub acel id, iar în browser resursa pornește direct în starea resolved, fără să mai apeleze loader-ul — tot doar cât e activ cache-ul de hidratare, adică până la prima stabilitate, și doar cu provideClientHydration() în configurație.

import { Component, inject, resource } from '@angular/core';

@Component({ selector: 'app-weather', template: `{{ forecast.value()?.summary }}` })
export class Weather {
  private readonly sdk = inject(WeatherSdk); // nu e HttpClient → cache-ul HTTP nu-l vede

  // v22: id → valoarea rezolvată pe server e pusă în TransferState și refolosită la hidratare.
  // id-ul trebuie să fie unic pe pagină și identic pe server și în browser.
  protected readonly forecast = resource({
    id: 'weather-bucharest',
    loader: () => this.sdk.forecast('Bucharest'),
  });
}

Origini diferite pe server și în browser

Dacă serverul vorbește cu API-ul pe o adresă internă, iar browserul pe cea publică, URL-urile diferă și cheile din cache nu se mai potrivesc. HTTP_TRANSFER_CACHE_ORIGIN_MAP (din @angular/common/http) îi spune serverului să calculeze cheia ca și cum ar fi folosit originea publică. Se declară doar în configurația de server: dacă îl găsește în browser, Angular aruncă eroarea NG02803.

// api-base-url.ts — în browser: originea publică
export const API_BASE_URL = new InjectionToken<string>('API_BASE_URL', {
  factory: () => 'https://api.magazin.ro',
});

// app.config.server.ts (fragment) — pe server: rețeaua internă, fără drum prin load balancer
providers: [
  provideServerRendering(withRoutes(serverRoutes)),
  { provide: API_BASE_URL, useValue: 'http://catalog.internal:8080' },
  // aceleași căi pe ambele origini → cheile din cache-ul de transfer se potrivesc
  // (doar în configurația de server; în browser Angular aruncă eroare dacă îl găsește)
  {
    provide: HTTP_TRANSFER_CACHE_ORIGIN_MAP,
    useValue: { 'http://catalog.internal:8080': 'https://api.magazin.ro' },
  },
];

Încă un detaliu din v22, când HttpClient folosește implicit backend-ul fetch: pe server, fiecare body de răspuns e limitat la 1 MB (altfel eroarea NG02825). Limita se schimbă cu provideServerRendering({ maxResponseBodySize }, withRoutes(…)), dar ghidul recomandă s-o ții cât mai mică: descărcările mari nu au ce căuta în randarea pe server.

Stabilitate și PendingTasks

Serverul nu știe când e „gata” pagina ta. Regula lui e simplă: după bootstrap așteaptă ApplicationRef.whenStable() și abia apoi serializează DOM-ul și TransferState. Aplicația e stabilă când nu mai e niciun task în PendingTasks. Angular adaugă singur task-uri pentru request-urile HttpClient în curs, pentru navigarea router-ului și pentru resursele care încarcă (resource, httpResource). Cu Zone.js, orice timer sau microtask din zona Angular ține și el aplicația instabilă.

Aplicațiile zoneless — implicite din v21 — nu mai văd timerele și promisiunile tale. Dacă încarci date cu un SDK propriu, cu fetch sau după un setTimeout, serverul serializează înainte să vină datele: HTML-ul pleacă gol, iar browserul le cere din nou. Le faci vizibile cu PendingTasks (stabil din v20; metoda run și operatorul pendingUntilEvent sunt încă developer preview):

import { Component, PendingTasks, inject, signal } from '@angular/core';
import { pendingUntilEvent, toSignal } from '@angular/core/rxjs-interop';

@Component({ selector: 'app-price', template: `{{ price() }} · {{ stock() }}` })
export class Price {
  private readonly tasks = inject(PendingTasks);
  private readonly sdk = inject(PricingSdk); // promisiuni proprii, nu HttpClient
  readonly price = signal<string | null>(null);

  // 1. run(): stabilitatea așteaptă până se termină funcția async (developer preview)
  constructor() {
    this.tasks.run(async () => this.price.set(await this.sdk.priceOf(3)));
  }

  // 2. add(): control manual — NU uita cleanup-ul, altfel serverul nu mai răspunde
  async refresh(): Promise<void> {
    const done = this.tasks.add();
    try {
      this.price.set(await this.sdk.priceOf(3));
    } finally {
      done();
    }
  }

  // 3. pendingUntilEvent(): instabil până la prima emisie / complete / eroare (developer preview)
  readonly stock = toSignal(this.sdk.stock$(3).pipe(pendingUntilEvent()), { initialValue: 0 });
}
LiveCând serializează serverul? (whenStable real, în browser)
appRef.isStable: true
Ce ar trimite serverul (snapshot la whenStable()):
<p class="price"></p>
Ambele butoane încarcă un preț în 1,2 secunde și apoi așteaptă appRef.whenStable() — exact ce face serverul înainte de serializare. Fără PendingTasks, whenStable se rezolvă imediat și „HTML-ul” pleacă gol; cu PendingTasks.run(), așteaptă datele. Urmărește și pastila isStable.
Un task care nu se termină = un request care nu se termină
Randarea pe server nu are timeout: dacă un task din PendingTasks nu e eliminat niciodată (un add() fără finally, un Observable care nu emite cu pendingUntilEvent), răspunsul atârnă până îl taie proxy-ul. Același lucru face, cu Zone.js, un setInterval pornit la inițializare. Polling-ul, websocket-urile și timerele recurente nu au ce căuta pe server: pornește-le în afterNextRender.
import { provideStabilityDebugging } from '@angular/core';

// Activ implicit în dev mode când folosești provideClientHydration().
// Manual: pentru SSR fără hidratare sau într-un build de producție, temporar.
bootstrapApplication(App, {
  providers: [provideStabilityDebugging()],
});
// Dacă aplicația nu devine stabilă în ~9 s, afișează în consolă task-urile din PendingTasks
// care o țin pe loc. Cu Zone.js, importă și 'zone.js/plugins/task-tracking' pentru stack-uri.

Request, response și status 404

@angular/core exportă trei tokeni pentru request-ul curent. @angular/ssr îi completează doar la randarea la cerere (RenderMode.Server); în browser, la prerender, la build și la extragerea rutelor în dev sunt null — deci întotdeauna ?..

TokenConțineFolosit pentru
REQUESTRequest din Web API: url, headerscookie-uri, Accept-Language, URL absolut pentru canonical / Open Graph
RESPONSE_INITResponseInit mutabil: status, statusText, headers404 / 410 dinamic, header-e calculate la randare
REQUEST_CONTEXTunknown: al doilea argument din handle(request, context)tenant, request id, utilizatorul autentificat deja de un middleware
import { Component, RESPONSE_INIT, inject } from '@angular/core';

@Component({
  selector: 'app-product-missing',
  template: `<h1>Produsul nu mai există</h1><a href="/produse">Vezi catalogul</a>`,
})
export class ProductMissing {
  constructor() {
    // null în browser, la prerender (SSG) și la build — de aceea ?.
    const response = inject(RESPONSE_INIT);
    if (response) response.status = 404; // altfel: „soft 404” indexat ca pagină reală
  }
}

De ce contează statusul: o pagină „Produsul nu există” servită cu 200 e un soft 404, pe care motoarele de căutare o pot indexa ca pe o pagină reală. status din ServerRoute acoperă cazurile statice (ruta **); pentru „id-ul nu există în baza de date” ai nevoie de RESPONSE_INIT. Merge pentru că @angular/ssr construiește răspunsul abia după whenStable(), deci până atunci poți modifica statusul.

import { isPlatformBrowser } from '@angular/common';
import { PLATFORM_ID, REQUEST, Service, inject } from '@angular/core';

@Service()
export class RequestInfo {
  private readonly request = inject(REQUEST); // Request din Web API; null în browser / SSG
  private readonly isBrowser = isPlatformBrowser(inject(PLATFORM_ID));

  /** Limba preferată: header-ul pe server, navigator în browser. */
  preferredLanguage(): string {
    if (this.request) return this.request.headers.get('accept-language')?.split(',')[0].trim() || 'ro';
    // fără request: browser SAU prerender. NU testa „typeof navigator” — Node 21+ îl are.
    return this.isBrowser ? navigator.language : 'ro';
  }

  cookie(name: string): string | null {
    const raw = this.request?.headers.get('cookie') ?? '';
    const match = raw.split('; ').find((c) => c.startsWith(`${name}=`));
    return match ? decodeURIComponent(match.slice(name.length + 1)) : null;
  }
}

URL-uri relative și absolute

Pe server nu există o „pagină curentă” față de care să se rezolve un URL relativ. HttpClient rezolvă singur problema: @angular/platform-server adaugă un interceptor care transformă /api/… în URL absolut, pe baza URL-ului request-ului și a lui <base href>. fetch() direct și SDK-urile care îl folosesc nu beneficiază de asta.

// ✓ HttpClient pe server: URL-ul relativ e rezolvat față de URL-ul request-ului curent
this.http.get('/api/products');          // → https://magazin.ro/api/products

// ✗ fetch() direct (sau un SDK care îl folosește) — Node nu are „pagină curentă”:
await fetch('/api/products');            // TypeError: Failed to parse URL from /api/products

// ✓ dacă trebuie fetch(): construiește URL-ul absolut din request
const base = inject(REQUEST)?.url ?? inject(DOCUMENT).location.href;
await fetch(new URL('/api/products', base));

Tocmai pentru că URL-urile se construiesc din host-ul request-ului, AngularNodeAppEngine verifică hostname-ul față de allowedHosts (în angular.json, la build.options.security.allowedHosts, sau prin variabila NG_ALLOWED_HOSTS) și răspunde 400 pentru un host necunoscut. E protecția împotriva SSRF: configureaz-o cu domeniile tale.

Stare scursă între request-uri

În browser, un modul JavaScript e evaluat o dată per tab, adică per utilizator. Pe server, același modul e evaluat o dată per proces și servește mii de request-uri. Orice variabilă la nivel de modul devine stare comună tuturor utilizatorilor. Un provider cu useValue are aceeași problemă: obiectul e creat la încărcarea modulului și trăiește până la restartul serverului — ghidul SSR o spune explicit și recomandă useFactory.

Serviciile sunt sigure: la fiecare request, @angular/ssr creează o platformă și o aplicație noi, deci un injector root nou. Fiecare utilizator primește instanțe proprii, distruse după răspuns.

// ✗ Starea la nivel de modul: fișierul e evaluat O SINGURĂ DATĂ per proces Node,
//    deci variabilele sunt comune TUTUROR request-urilor (și tuturor utilizatorilor).
let currentUser: User | null = null;          // utilizatorul B îl vede pe A
const cart = new Map<number, number>();       // coșul crește cu fiecare request

// ✗ La fel cu useValue: obiectul e creat o dată, la încărcarea modulului
{ provide: CART_STATE, useValue: { items: [] } }

// ✓ Un serviciu: pe server fiecare request bootstrap-ează o aplicație nouă,
//    deci primește o instanță nouă (și e distrusă după răspuns).
@Service()
export class CartStore {
  private readonly items = signal<CartItem[]>([]);
  readonly count = computed(() => this.items().length);
}

// ✓ Sau useFactory: rulează pentru fiecare aplicație creată
{ provide: CART_STATE, useFactory: () => ({ items: [] }) }
Cache-ul „inofensiv” de lângă serviciu
const cache = new Map() la nivel de modul pare o optimizare. Pe server e o scurgere de memorie (crește la nesfârșit) și, dacă cheia nu include utilizatorul, o scurgere de date între conturi. Dacă vrei cache între request-uri, fă-l explicit — în server.ts sau la CDN — cu chei, TTL și limită de mărime.

Studiu de caz: tema

Tema e exemplul clasic: preferința stă în localStorage, iar CSS-ul depinde de un atribut pe <html>. Acest site e SPA, fără SSR, deci ThemeStore-ul lui e corect aici. Dar e un exercițiu bun: dacă am porni SSR mâine, ce s-ar strica?

// ThemeStore-ul acestui site (fragment). Site-ul e SPA, deci e corect AICI.
@Service()
export class ThemeStore {
  private readonly document = inject(DOCUMENT);
  // ✗ pe server: defaultView e fereastra Domino, care NU are matchMedia → TypeError
  private readonly media = this.document.defaultView?.matchMedia('(prefers-color-scheme: dark)');

  // readStoredMode() citește localStorage într-un try/catch: pe server nu cade,
  //    dar întoarce mereu 'system' — serverul nu poate ști ce a ales utilizatorul.
  readonly mode = signal<ThemeMode>(readStoredMode());

  constructor() {
    this.media?.addEventListener('change', (e) => this.systemDark.set(e.matches));
    effect(() => {
      // ✓ DOM prin DOCUMENT: merge și pe server
      // localStorage în try/catch: pe server e ignorat în tăcere
    });
  }
}

Două probleme. Prima: linia cu matchMedia aruncă la crearea serviciului, iar ?. nu ajută, fiindcă pe server defaultView există (fragmentul defaultView?.matchMedia(…) din primul demo reproduce exact asta). A doua: serverul randează mereu tema system, pentru că nu vede localStorage. Varianta SSR-safe mută preferința într-un cookie, pe care serverul îl citește din REQUEST, iar effect()-ul scrie atributul și pe server:

import { isPlatformBrowser } from '@angular/common';
import {
  DOCUMENT, DestroyRef, PLATFORM_ID, REQUEST, Service,
  afterNextRender, computed, effect, inject, signal,
} from '@angular/core';

export type ThemeMode = 'system' | 'light' | 'dark';
const COOKIE = 'atlas-theme';

@Service()
export class ThemeStore {
  private readonly document = inject(DOCUMENT);
  private readonly request = inject(REQUEST);
  private readonly isBrowser = isPlatformBrowser(inject(PLATFORM_ID));

  // Serverul nu știe dacă sistemul e dark: pentru 'system' decide CSS-ul
  // (prefers-color-scheme), iar semnalul se corectează în browser.
  private readonly systemDark = signal(false);
  readonly mode = signal<ThemeMode>(this.readCookie());
  readonly isDark = computed(
    () => this.mode() === 'dark' || (this.mode() === 'system' && this.systemDark()),
  );

  constructor() {
    const destroyRef = inject(DestroyRef);
    afterNextRender(() => {
      const media = matchMedia('(prefers-color-scheme: dark)'); // doar în browser
      const onChange = (e: MediaQueryListEvent) => this.systemDark.set(e.matches);
      this.systemDark.set(media.matches);
      media.addEventListener('change', onChange);
      destroyRef.onDestroy(() => media.removeEventListener('change', onChange));
    });

    effect(() => {
      const mode = this.mode();
      // rulează și pe server → <html data-theme="dark"> pleacă deja în HTML, fără flash
      const root = this.document.documentElement;
      if (mode === 'system') root.removeAttribute('data-theme');
      else root.setAttribute('data-theme', mode);
      // cookie (nu localStorage): îl primește serverul la următorul request
      if (this.isBrowser) {
        this.document.cookie = `${COOKIE}=${mode}; path=/; max-age=31536000; samesite=lax`;
      }
    });
  }

  private readCookie(): ThemeMode {
    const raw = this.request?.headers.get('cookie') // server: header-ul request-ului
      ?? (this.isBrowser ? this.document.cookie : ''); // browser (pe server, cookie aruncă)
    const value = raw
      .split('; ')
      .find((c) => c.startsWith(`${COOKIE}=`))
      ?.slice(COOKIE.length + 1);
    return value === 'light' || value === 'dark' ? value : 'system';
  }
}

Alternativa fără cookie: păstrezi localStorage și un script inline în index.html, care setează data-theme înainte de prima afișare — exact ce face acest site. Merge și cu SSR (serverul trimite HTML fără atribut, scriptul îl pune în browser înaintea aplicației), dar serverul tot nu știe tema, deci nu poate alege, de exemplu, imaginile sau theme-color potrivite.

<!-- Alternativa fără cookie (ce face acest site): un script inline în <head>,
     care rulează înainte de prima afișare, în orice mod de randare (CSR, SSR, prerender). -->
<script>
  try {
    var t = localStorage.getItem('atlas-theme');
    if (t === 'light' || t === 'dark') document.documentElement.setAttribute('data-theme', t);
  } catch (e) {}
</script>

Când DA, când NU

Cod SSR-safe nu înseamnă if (isBrowser) peste tot, ci fiecare lucru pus în locul potrivit.

Folosește
  • afterNextRender / afterRenderEffect pentru tot ce atinge DOM-ul, layout-ul, storage-ul sau o bibliotecă de browser.
  • inject(DOCUMENT), Title, Meta pentru tot ce trebuie să fie în HTML-ul de pe server (SEO, share).
  • Provideri pe platformă în app.config.server.ts când serverul și browserul fac lucruri diferite.
  • Cache-ul HTTP implicit pentru GET-uri publice; TransferState manual sau resource({ id }) pentru date care nu trec prin HttpClient.
  • PendingTasks sau resource() pentru orice async al cărui rezultat trebuie să fie în HTML.
  • RESPONSE_INIT pentru status-uri dinamice, REQUEST pentru cookie-uri și limbă.
Evită (și ce pui în loc)
  • isPlatformBrowser într-un @if din template → @defer sau afterNextRender, cu același HTML pe ambele platforme.
  • typeof window presărat prin componente → un serviciu cu implementare separată pentru server.
  • TransferState pentru liste mari sau date per utilizator pe pagini cache-uite la CDN → încarcă-le în browser.
  • PendingTasks pentru polling, websocket, analytics → pornește-le în afterNextRender; serverul nu trebuie să le aștepte. Un exemplu complet e pe pagina WebSockets.
  • Cache HTTP pentru răspunsuri sensibile → filter sau transferCache: false.
  • ngSkipHydration ca soluție permanentă → repară manipularea directă a DOM-ului.

Capcane frecvente

effect() rulează și pe server
Mulți tratează effect() ca pe un „hook de browser”. Nu e: rulează la change detection, deci și pe server. Un effect care scrie în localStorage, apelează matchMedia sau pornește un observer cade la randare. Ce ține de browser merge în afterRenderEffect sau sub afterNextRender.
?. nu e o gardă de platformă
document.defaultView?.matchMedia(…) și typeof navigator !== 'undefined' par sigure, dar pe server defaultView există (fereastra Domino, fără matchMedia), iar Node 21+ are navigator. Primul aruncă TypeError, al doilea dă răspunsul greșit. Folosește afterNextRender sau isPlatformBrowser.
  • Măsurători în constructor sau ngOnInit: pe server cad (nu există layout). În browser, constructorul măsoară 0, iar în ngOnInit binding-urile și blocurile @if / @for ale componentei nu sunt încă randate. Fix: afterNextRender({ read }).
  • Timere și subscripții pornite în constructor: cu Zone.js blochează serializarea, zoneless rămân pornite. Fix: pornește-le în afterNextRender și oprește-le cu DestroyRef.
  • pendingTasks.add() fără finally: o excepție și request-ul nu se mai termină. Fix: try/finally sau run().
  • Async neurmărit în zoneless: fetch, SDK-uri, setTimeout → HTML gol + încă un request în browser. Fix: resource() sau PendingTasks.
  • Chei TransferState generice ('data') sau un resource({ id }) static într-o componentă folosită de două ori pe pagină: valorile se suprascriu. Fix: chei care includ parametrii (article:${slug}), id-uri unice pe pagină.
  • Cheia nu e ștearsă după citire: o navigare ulterioară pe aceeași pagină primește datele vechi din HTML. Fix: state.remove(key) după prima folosire.
  • HTTP_TRANSFER_CACHE_ORIGIN_MAP în app.config.ts: eroare NG02803 în browser. Fix: doar în app.config.server.ts.
  • fetch('/api/…') relativ pe server: TypeError: Failed to parse URL. Fix: HttpClient sau URL absolut construit din REQUEST.
  • Pagina de „nu există” cu status 200: soft 404 în Google. Fix: RESPONSE_INIT sau status: 404 pe ruta **.
  • Valori care diferă între server și browser: Date.now(), Math.random(), date formatate în fusul orar al serverului (adesea UTC). Textul se schimbă la hidratare. Fix: generează-le o dată pe server și transferă-le, sau afișează-le abia în browser.
  • Stare la nivel de modul și useValue cu obiecte: partajate între utilizatori. Fix: servicii sau useFactory.

Fișă rapidă

Vrei să…Folosește
atingi DOM-ul / layout-ul o dată, după randareafterNextRender({ earlyRead, write, read })
sincronizezi DOM-ul cu niște semnaleafterRenderEffect(() => …)
măsori după fiecare randareafterEveryRender({ read })
încarci o bibliotecă care atinge windowawait import('lib') în afterNextRender, sau @defer
citești / scrii title, meta, canonicalinject(DOCUMENT), Title, Meta
altă implementare pe serverclasă abstractă + provider în app.config.server.ts (mergeApplicationConfig)
un efect secundar doar în browserisPlatformBrowser(inject(PLATFORM_ID))
să nu repeți GET-urile în browserprovideClientHydration() (implicit), withHttpTransferCacheOptions(…)
scoți un request din cachefilter sau { transferCache: false }
transferi date care nu vin prin HttpClientmakeStateKey<T>() + TransferState, sau resource({ id }) (v22)
origini API diferite pe server și în browserHTTP_TRANSFER_CACHE_ORIGIN_MAP, doar pe server
ca serverul să aștepte un asyncresource(), PendingTasks.run() / add(), pendingUntilEvent()
afli de ce aplicația nu devine stabilăprovideStabilityDebugging()
citești cookie-uri / header-einject(REQUEST)?.headers.get('cookie')
răspunzi cu 404inject(RESPONSE_INIT) → status = 404, sau status: 404 pe ruta **
primești context de la server.tshandle(req, ctx) → inject(REQUEST_CONTEXT)
stare per utilizator pe serverservicii sau useFactory — niciodată variabile de modul

Cap. 12 · 0 din 3 terminate

Ai terminat lecția?

Marcheaz-o și apare pe harta ta: în cuprins și pe prima pagină.

Se marchează singură după ce citești toată lecția.

  1. SSR, prerender & render modes
  2. Hidratare: event replay & incremental
  3. Cod compatibil cu serverul
Vezi toată harta ta

Toate cele 75 de pagini · scrie ca să filtrezi

  • 01Ce e nou în Angular 21 & 22Toate schimbările importante din v21 și v22, pe scurt, cu link către pagina care le explică.Start
  • 01Prima ta aplicație, pas cu pasO listă de lecturi construită de la o componentă goală la o aplicație terminată — signals, @for, input-uri și output-uri, un serviciu și un formular, pas cu pas.Start
  • 01Decizii rapide„Unde pun asta?”, „Signal sau Observable?”, „Shared sau feature?” — răspunsuri scurte la întrebările de zi cu zi.Start
  • 01Întrebări de interviu Angular60 de întrebări reale de interviu pentru junior, mid și senior, cu răspunsul de dat, întrebarea care urmează și greșelile de evitat. Plus o simulare de interviu.Start
  • 02Standalone & bootstrapCum pornește o aplicație fără NgModule: bootstrapApplication, app.config.ts, imports în componentă.Fundamente moderne
  • 02Data binding și evenimenteInterpolare, binding pe proprietăți și pe atribute, evenimente cu $event și filtre de taste și cum e construit [( )] — ce intră în DOM și ce se întoarce.Fundamente moderne
  • 02Control flow: @if, @for, @switchSintaxa de blocuri care înlocuiește *ngIf/*ngFor/ngSwitch, cu track, @empty și verificare exhaustivă.Fundamente moderne
  • 02@let și expresiile noi din templateVariabile locale cu @let, arrow functions, spread, regex, instanceof, comentarii în tag-uri.Fundamente moderne
  • 02@defer — lazy loading în templateÎncarci componente grele doar când e nevoie: triggere, prefetch, placeholder, loading, error.Fundamente moderne
  • 02Class & style bindingsDe ce [class.x] și [style.x] au înlocuit ngClass și ngStyle (cu migrare automată din v21).Fundamente moderne
  • 02Stilurile componentelor și încapsularea:host, cele trei moduri de încapsulare, teme cu variabile CSS și de ce ::ng-deep e ultima soluție.Fundamente moderne
  • 02Pipes: când și cumPipe pur vs impur, pipe custom și cum alegi între pipe, computed() și o metodă.Fundamente moderne
  • 03signal() și computed()Primitivele reactive: stare scriibilă, stare derivată, egalitate, untracked și reguli de imutabilitate.Signals & reactivitate
  • 03effect() — când DA, când NUEfecte secundare sincronizate cu semnale: localStorage, logging, API-uri imperative. Și capcanele lor.Signals & reactivitate
  • 03linkedSignal() — stare derivată editabilăUn semnal care se resetează când se schimbă sursa, dar pe care îl poți și seta manual.Signals & reactivitate
  • 03resource, rxResource, httpResourceDate asincrone ca semnale: status, value, error, reload, anulare automată. Stabile din v22.Signals & reactivitate
  • 03debounced() — semnale cu întârziereVarianta „debounce” a unui semnal, fără RxJS. Perfect pentru căutare.Signals & reactivitate
  • 03Liste: căutare, filtrare, sortare, paginareUn lanț de computed-uri care recalculează doar ce s-a schimbat, cu filtrele ținute în URL, ca un link să redeschidă aceeași vedere.Signals & reactivitate
  • 03Observables de la zeroCe e un Observable, subscribe și unsubscribe, operatorii pe care îi folosești în fiecare săptămână, cold vs hot — și când ajunge un signal.Signals & reactivitate
  • 03Signals ↔ RxJStoSignal, toObservable, takeUntilDestroyed, outputFromObservable — și când RxJS rămâne alegerea bună.Signals & reactivitate
  • 04input() și output()Inputuri ca semnale (required, transform, alias) și outputuri tipizate. Data down, events up.Componente: comunicare
  • 04model() — two-way bindingBanana-in-a-box [( )] cu un singur API. Când îl folosești și când e mai bine input + output.Componente: comunicare
  • 04viewChild & contentChildInterogări ca semnale: viewChild, viewChildren, contentChild, contentChildren, opțiunea read.Componente: comunicare
  • 04Lifecycle & DestroyRefCe hook-uri mai contează în era signals, afterNextRender, DestroyRef și ngOnChanges tipizat.Componente: comunicare
  • 04Content projectionng-content cu select, sloturi multiple, conținut implicit și capcana proiecției condiționale.Componente: comunicare
  • 04ng-template & ngTemplateOutletTemplate-uri ca valori: render props în Angular, context tipizat și personalizare de componente.Componente: comunicare
  • 05Smart vs DumbContainer vs presentational: cine ține starea, cine doar afișează, și cum le împarți corect.Design de componente
  • 05Componente generice <T>Un DataTable<T> sau Select<T> tipizat corect, cu template-uri pentru celule.Design de componente
  • 05Componente dinamiceNgComponentOutlet, createComponent cu inputBinding/outputBinding și un dashboard construit din config.Design de componente
  • 05Directive & host compositionDirective atribut, metadata host: {} și Directive Composition API (hostDirectives).Design de componente
  • 05Controale custom de formularFormValueControl (Signal Forms) vs ControlValueAccessor: un rating cu stele, în ambele variante.Design de componente
  • 05Change detectionOnPush e implicit din v22, zone.js lipsește din v21. Ce declanșează randarea și cum să nu te arzi.Design de componente
  • 06inject() & @Service()Noul decorator @Service() din v22, inject() vs constructor și contextul de injecție.Dependency Injection
  • 06Providers & ierarhia de injectoriRoot vs rută vs componentă, useClass/useValue/useFactory/useExisting și modificatorii optional/self/skipSelf.Dependency Injection
  • 06InjectionToken & provideX()Configurare tipizată, funcții provideFeature(), makeEnvironmentProviders și inițializatori.Dependency Injection
  • 06injectAsync() — servicii lazyÎncarci un serviciu greu abia când ai nevoie de el, cu prefetch la idle.Dependency Injection
  • 07Structura de folderecore / shared / features, ce intră în fiecare și cum arată un feature pe dinăuntru.Arhitectura aplicației
  • 07Shared vs featureCând muți o componentă în shared și (mai ales) când NU. Checklist concret.Arhitectura aplicației
  • 07Interfețe & modeleInterfață inline sau fișier separat? DTO vs model de domeniu, type vs interface, unde le pui.Arhitectura aplicației
  • 07State management cu signalsStore-uri simple pe semnale, facade, stare locală vs globală și când îți trebuie NgRx.Arhitectura aplicației
  • 07Style guide & namingConvențiile oficiale actuale: nume de fișiere fără sufixe, protected, readonly, ordinea membrilor.Arhitectura aplicației
  • 07Barrels, aliasuri & granițeindex.ts, path aliases, API public pe feature și reguli de dependență care se pot verifica automat.Arhitectura aplicației
  • 07Erori: @boundary & ErrorHandlerNoul bloc @boundary din v22.2, ErrorHandler global, erori HTTP și erori din resource.Arhitectura aplicației
  • 08Routing modern & lazy loadingloadComponent, rute copil, parametri ca input-uri și moștenirea parametrilor (default nou în v22).Routing
  • 08Guards & resolverscanActivate, canMatch, canDeactivate și resolve ca funcții, cu redirect prin UrlTree / RedirectCommand.Routing
  • 09Signal Forms — bazeleFormulare construite pe semnale: form(), [formField], validare prin schemă. Stabile din v22.Formulare
  • 09Signal Forms — avansatValidare custom și cross-field, async/HTTP cu debounce, liste dinamice, disabled/hidden, scheme reutilizabile.Formulare
  • 09Reactive Forms tipizateFormGroup tipizat, nonNullable, FormArray — și cum conviețuiesc cu Signal Forms.Formulare
  • 10HttpClient, interceptors & API layerHttpClient fără setup (v21), Fetch implicit (v22), interceptori funcționali și stratul de API cu mapare DTO.HTTP & date
  • 11Performanță — checklistCe chiar contează: OnPush + signals, track, @defer, lazy routes, imagini, bundle și hidratare.Calitate & performanță
  • 11Testare cu VitestVitest e runner-ul implicit din v21. TestBed, componente cu semnale, servicii, HTTP și harness-uri.Calitate & performanță
  • 11DevTools: cum te uiți în aplicațieChrome DevTools și Angular DevTools pas cu pas: ce arată fiecare panou, cu exerciții care merg chiar pe site-ul ăsta.Calitate & performanță
  • 11Angular AriaComponente headless, accesibile, pe care le stilizezi cum vrei: tabs, accordion, menu, listbox...Calitate & performanță
  • 12SSR, prerender & render modesCând randezi pe server, când la build și când doar în browser: RenderMode pe rută, app.routes.server.ts și costurile reale.SSR & hidratare
  • 12Hidratare: event replay & incrementalCum preia browserul HTML-ul de pe server fără să-l refacă: hidratare completă, event replay, @defer (hydrate on …) și erorile de nepotrivire.SSR & hidratare
  • 12Cod compatibil cu serverulwindow nu există pe server: PLATFORM_ID, afterNextRender, DOCUMENT, TransferState, cache-ul HTTP și PendingTasks.SSR & hidratare
  • 13Micro frontendsCând merită să spargi o aplicație în mai multe deploy-uri și când e doar complexitate: Native Federation, Module Federation, iframes, Web Components.Arhitectură la scară
  • 13Web Components cu Angular ElementscreateCustomElement: o componentă Angular folosită ca tag HTML oriunde, plus comunicarea între aplicații prin evenimente și BroadcastChannel.Arhitectură la scară
  • 13Monorepo, librării & granițeUn workspace cu mai multe aplicații și librării: ng generate library, Nx, entry points secundare, publicare și reguli de dependență.Arhitectură la scară
  • 13Arhitectură hexagonală (ports & adapters)Domeniul fără Angular, porturi ca clase abstracte și adaptoare interschimbabile: HTTP, localStorage sau in-memory, fără să atingi UI-ul.Arhitectură la scară
  • 14Semnalele pe dinăuntruGraful reactiv: producători și consumatori, push-pull, de ce computed e lazy și glitch-free, problema diamantului și când rulează efectele.Reactivitate avansată
  • 14RxJS avansat: operatori de ordin superiorswitchMap, mergeMap, concatMap și exhaustMap pe o diagramă live, plus retry cu backoff, shareReplay și cum eviți scurgerile.Reactivitate avansată
  • 14NgRx SignalStoreState management la scară pe semnale: withState, withComputed, withMethods, rxMethod, entități și hooks — și când un store simplu e suficient.Reactivitate avansată
  • 14Mașini de stări cu signalsFără stări imposibile: o mașină tipizată pe signals, cu tranziții, guards și diagrama stărilor, live.Reactivitate avansată
  • 15WebSockets: timp real care rezistă rețeleiUn socket tipizat ca signals: reconectare cu backoff și jitter, heartbeat, coadă de mesaje cât ești offline și siguranță la SSR.Timp real și offline
  • 15Actualizări optimiste și rollbackInterfață instantanee, dar onestă: snapshot și rollback, răspunsuri sosite în altă ordine, chei de idempotență.Timp real și offline
  • 15Offline-first: Service Worker și cacheService Worker-ul Angular: strategii de cache, anunțarea versiunilor noi și o coadă pentru modificările făcute offline.Timp real și offline
  • 16Web WorkersCalcule grele fără să blochezi interfața: un worker real în pagină, comunicare prin mesaje și când nu merită.UI & performanță avansată
  • 16Liste uriașe: virtual scroll100.000 de rânduri fără lag: CDK virtual scroll, itemSize, trackBy și alternativele (paginare, infinite scroll).UI & performanță avansată
  • 16Animații moderne & View Transitionsanimate.enter și animate.leave în locul @angular/animations (depreciat), tranziții CSS și View Transitions la navigare.UI & performanță avansată
  • 16Memory leaks: cum le găsești și le repariSubscripții, timere și listeneri care trăiesc mai mult decât componenta lor — cum îi vezi în DevTools și cum îi închizi.UI & performanță avansată
  • 17Securitate: XSS, sanitizare & CSPCe face Angular automat împotriva XSS, când devine periculos bypassSecurityTrust, plus CSP cu nonce, Trusted Types și XSRF.Securitate & globalizare
  • 17Autentificare: token-uri & refreshUn interceptor care atașează token-ul, reîmprospătează o singură dată la 401 și reia cererile din coadă — plus unde ții token-ul.Securitate & globalizare
  • 17OAuth 2.1 și OpenID Connect cu PKCEFluxul authorization code cu PKCE, pas cu pas: verifier și challenge, state și nonce, ID token-ul și de ce un BFF e mai sigur.Securitate & globalizare
  • 17Internaționalizare (i18n)$localize, atributul i18n, ICU plural/select, build per limbă vs. traduceri la runtime și formatarea după locale.Securitate & globalizare