AngularAtlased. 22.2

Cap. 14Reactivitate avansată64 / 75

Mașini de stări cu signals

Fără stări imposibile: o mașină tipizată pe signals, cu tranziții, guards și diagrama stărilor, live.

arhitectură ~0 min de citit

Majoritatea bug-urilor de interfață nu sunt calcule greșite, ci combinații imposibile: un spinner și un mesaj de eroare în același timp, un buton „Plătește” care încă merge cât timp plata rulează, un wizard care a sărit un pas. Vin din descrierea unui ecran cu booleeni independenți — isLoading, hasError, isPaid. Trei booleeni fac opt combinații; ecranul are poate patru legitime.

O mașină de stări întoarce întrebarea pe dos: listezi stările care pot exista cu adevărat și, pentru fiecare, evenimentele pe care le acceptă. Tot restul devine imposibil prin construcție, nu prin disciplină. Pe signals are cam treizeci de linii și nicio bibliotecă.

De ce contează
Signal-urile fac starea derivată ușoară, dar nu te opresc să setezi două signal-uri care se contrazic. Mașina e regula care lipsea: un singur signal ține starea, iar singurul mod de a o schimba e o tranziție pe care ai declarat-o.

Trei booleeni, opt combinații

Iată versiunea obișnuită. Fiecare handler își setează propriul flag și uită să-l reseteze pe altul — nu din neatenție, ci pentru că nimic nu te obligă să te gândești la toate deodată. Demo-ul de mai jos rulează exact logica asta în stânga și o mașină în dreapta.

// ✗ trei flaguri independente = 8 combinații; ecranul are 4 legitime
loading = signal(false);
error = signal<string | null>(null);
data = signal<Order[] | null>(null);

load() {
  this.loading.set(true); // uitat: error.set(null) — eroarea veche e încă pe ecran
  this.api.orders().subscribe({
    next: (orders) => this.data.set(orders), // uitat: loading.set(false)
    error: (e) => this.error.set(e.message), // uitat: loading.set(false) — spinner ȘI eroare
  });
}
LiveAceleași evenimente: booleeni vs o mașină

Trei booleeni

loading=false · error=false · data=false

Nimic încărcat încă

Combinații imposibile atinse: 0

O mașină

state = 'idle'

Nimic încărcat încă

Butoane dezactivate = evenimente pe care starea asta nu le acceptă.

În stânga, apasă LOAD, apoi FAIL: spinner-ul și eroarea apar împreună. În dreapta, aceeași ordine e imposibilă — butoanele pe care starea curentă nu le acceptă sunt dezactivate.

Primul pas: o uniune discriminată

Înainte de orice mașină, pune starea într-o singură valoare a cărei formă depinde de status. data există doar în success, iar error doar în error, așa că „o eroare împreună cu date” nici nu poate fi scrisă. În template, @switch îngustează tipul: în @case ('error'), o.error e un string, nu poate-undefined.

// ✓ o singură valoare; `data` există doar în success, `error` doar în error
type Load<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: string };

readonly orders = signal<Load<Order[]>>({ status: 'idle' });

Cu @default never; (Angular 21.2), dacă adaugi un status nou în tip și îl uiți în template, primești o eroare de compilare. Mai multe despre noul control flow în @if, @for, @switch.

O mașină pe signals, în treizeci de linii

O uniune spune ce sunt stările; o mașină spune și cum treci între ele. Inima e un tabel: pentru fiecare stare, evenimentele pe care le acceptă și unde duce fiecare. send() se uită în tabel; un eveniment pe care starea curentă nu îl acceptă e pur și simplu ignorat. can() răspunde la aceeași întrebare pentru template, ca butoanele să nu copieze regulile în [disabled]. Guard-urile adaugă condiții pe date, iar onEnter pune efectele într-un singur loc.

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

export type Transitions<S extends string, E extends string> = {
  readonly [state in S]: Partial<Record<E, S>>;
};

export interface MachineOptions<S extends string, E extends string> {
  /** condiții în plus: evenimentul e permis doar dacă guard-ul lui întoarce true */
  readonly guards?: Partial<Record<E, (from: S) => boolean>>;
  /** efecte la intrarea într-o stare — un singur loc pentru apeluri API, timere, analytics */
  readonly onEnter?: Partial<Record<S, () => void>>;
}

export function createMachine<S extends string, E extends string>(
  initial: S,
  transitions: Transitions<S, E>,
  options: MachineOptions<S, E> = {},
) {
  const state = signal<S>(initial);
  const target = (event: E) => transitions[state()][event];
  const can = (event: E) =>
    target(event) !== undefined && (options.guards?.[event]?.(state()) ?? true);

  return {
    state: state.asReadonly(), // nimeni nu îl poate .set() din afară
    can, // pentru [disabled], în loc să copiezi condiții în template-uri
    send(event: E): boolean {
      if (!can(event)) return false; // nepermis în starea asta: nu se întâmplă nimic
      const next = target(event)!;
      state.set(next);
      options.onEnter?.[next]?.();
      return true;
    },
  };
}

Un checkout, cu diagrama de stări live

Un flux de plată e cazul clasic: înapoi e permis din livrare, dar nu cât timp plata rulează, nu poți ieși din livrare fără adresă, iar un card refuzat are nevoie de un drum înapoi la pasul de plată. Toate astea sunt linii într-un tabel.

LiveCheckout: stări, evenimente, guard-uri
NEXT NEXT PAY PAID DECLINED BACK BACK RETRY cartshippingpaymentpayingpaidfailed

Două produse în coș. NEXT duce la livrare.

  1. Aici apar fiecare eveniment și tranziția pe care a produs-o.
Parcurge pașii. În „shipping”, NEXT rămâne dezactivat până scrii o adresă (un guard). În timpul „paying”, doar banca poate mișca mașina: încearcă BACK. Bifează „banca refuză cardul” ca să vezi ieșirea din „failed”.
import { Service, inject, signal } from '@angular/core';
import { firstValueFrom } from 'rxjs';
import { createMachine } from './machine';
import { PaymentsApi } from './payments-api';

type Step = 'cart' | 'shipping' | 'payment' | 'paying' | 'paid' | 'failed';
type CheckoutEvent = 'NEXT' | 'BACK' | 'PAY' | 'PAID' | 'DECLINED' | 'RETRY';

@Service()
export class CheckoutStore {
  private readonly payments = inject(PaymentsApi);
  readonly address = signal('');
  readonly error = signal<string | null>(null);

  readonly machine = createMachine<Step, CheckoutEvent>(
    'cart',
    {
      cart: { NEXT: 'shipping' },
      shipping: { NEXT: 'payment', BACK: 'cart' },
      payment: { PAY: 'paying', BACK: 'shipping' },
      paying: { PAID: 'paid', DECLINED: 'failed' }, // fără BACK, fără al doilea PAY cât timp se plătește
      failed: { RETRY: 'payment' },
      paid: {}, // stare finală
    },
    {
      guards: {
        // guard-ul primește starea curentă: nicio referință la `this.machine` în propria ei definiție
        NEXT: (from) => from !== 'shipping' || this.address().trim() !== '',
      },
      onEnter: { paying: () => void this.pay() },
    },
  );

  private async pay(): Promise<void> {
    try {
      await firstValueFrom(this.payments.charge());
      this.machine.send('PAID');
    } catch (e) {
      this.error.set((e as Error).message);
      this.machine.send('DECLINED');
    }
  }
}

Uită-te la store: template-ul nu decide niciodată ce butoane sunt permise, iar nicio componentă nu apelează API-ul de plăți. Banca e apelată într-un singur loc — la intrarea în paying — așa că un dublu click, un buton de înapoi sau o componentă randată de două ori nu pot debita cardul de două ori.

Când folosești o bibliotecă: XState

O mașină scrisă de mână ajunge pentru un flux pe un ecran. Când stările se imbrică (un checkout într-o aplicație care poate trece offline), rulează în paralel (upload și formular în același timp), depind de timere sau vrei un editor vizual și teste generate, XState e biblioteca standard pentru statecharts. Ideea nu se schimbă: stări, evenimente, tranziții. Și starea conexiunii de pe pagina WebSockets e o mică mașină.

Când DA, când NU

Când DA
  • Fluxuri în mai mulți pași: checkout, onboarding, wizard-uri, aprobări.
  • Orice e asincron cu un status vizibil: încărcare, eroare, reîncercare, anulare.
  • Controale cu moduri (vizualizare, editare, drag, preview) și conexiuni care vin și pleacă — vezi WebSockets.
Când NU
  • Un simplu toggle: un signal boolean e perfect.
  • Valori derivate din altele: acolo e computed(), nu o stare.
  • Formulare cu câmpuri independente: fiecare câmp e propria lui stare; biblioteca de formulare știe deja valid, dirty și pending.

Capcane frecvente

  • Un boolean pentru fiecare bucată de ecran. Fiecare flag nou dublează combinațiile pe care nu le-a testat nimeni.
  • Un string de status fără tabel. status.set('paid') de oriunde înseamnă tot booleeni, într-o singură variabilă.
  • Reguli copiate în template-uri. [disabled]="step === 'paying' || !address" în trei locuri; întreabă can() în schimb.
  • Efecte împrăștiate prin componente. Două componente reacționează la „paying” și cardul e debitat de două ori. Folosește onEnter, o singură dată.
  • O stare fără ieșire. „failed” fără RETRY sau BACK, iar singura scăpare e reîncărcarea paginii.

Fișă rapidă

simptom              un spinner și o eroare împreună, un buton care merge când n-ar trebui
cauză                booleeni independenți: n flaguri = 2^n combinații, majoritatea imposibile
primul pas           o uniune discriminată: { status: 'success'; data } | { status: 'error'; error }
mașină               un signal de stare + un tabel  stare → { EVENIMENT: starea următoare }
send(event)          permis doar dacă tabelul îl are (și guard-ul spune da); altfel ignorat
can(event)           pentru [disabled] și ascunderea acțiunilor, niciodată condiții duplicate
guards               condiții pe date (o adresă, un formular valid) lângă tabel
onEnter              efectele într-UN singur loc: intrarea în 'paying' apelează API-ul
stări finale         {}: nimic nu poate ieși din 'paid'
orice stare          are nevoie de o ieșire (failed → RETRY), altfel utilizatorii rămân blocați
valori derivate      tot computed(); mașina decide doar ce SE POATE întâmpla
imbricat / paralel   stări în stări, timere, un editor vizual → XState

Cap. 14 · 0 din 4 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. Semnalele pe dinăuntru
  2. RxJS avansat: operatori de ordin superior
  3. NgRx SignalStore
  4. Mașini de stări cu signals
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