Të gjithë artikujt

Nga RxJS te Signals: udhëzues praktik për migrimin e gjendjes në Angular

Hyrje: revolucioni i reaktivitetit në Angular

Që nga versionet e para të Angular, RxJS ka qenë motori i reaktivitetit: thirrjet HTTP, formularët, router-i dhe ngjarjet kalonin të gjitha përmes Observable-ve. Një mjet shumë i fuqishëm, por edhe i kërkueshëm: për të shfaqur një vlerë të thjeshtë të derivuar duheshin subject-e, operatorë dhe subscription-e për t'u mbyllur.

Me Signals ekipi i Angular ka sjellë një model më të thjeshtë për gjendjen e aplikacionit: një vlerë që lexohet, përditësohet dhe nga e cila varen automatikisht vlera të tjera. Rezultati është më pak kod, lexim më i drejtpërdrejtë dhe themelet për një Angular pa Zone.js. Në këtë artikull shohim si dhe kur ta kalojmë gjendjen nga RxJS te Signals, me një rifaktorizim të plotë.

RxJS kundrejt Signals: dy mjete për probleme të ndryshme

Dallimi më i dobishëm është ky: RxJS përshkruan ngjarje në kohë — klikime, mesazhe WebSocket, përgjigje HTTP, shtypje tastiere për t'u ngadalësuar — ndërsa Signals përshkruajnë gjendjen aktuale dhe vlerat që rrjedhin prej saj. Nuk janë konkurrentë: zgjidhin probleme të ndryshme, siç e kam shpjeguar më në detaje në këtë artikull.

RxJSSignalsShënime
BehaviorSubject<T>signal<T>()Vlera aktuale, e lexueshme në mënyrë sinkrone
combineLatest() + map()computed()Varësitë gjurmohen automatikisht
.subscribe() për efekte anësoreeffect()Vetëm drejt botës së jashtme
Pipe-i asyncleximi i vlera() në templateAsnjë subscription për t'u menaxhuar
debounceTime, switchMapmbeten RxJSUrë me toObservable

Ka edhe një avantazh në zbulimin e ndryshimeve: kur një template lexon një signal, Angular e di saktësisht cilët komponentë varen nga ajo vlerë dhe përditëson vetëm ata, në vend që të rikontrollojë gjithë pemën e komponentëve në çdo ngjarje.

Anatomia e rifaktorizimit: një rast real

Skenari është klasik: një dashboard me listën e produkteve, një fushë kërkimi teksti dhe numërimin e elementeve të filtruar. Ja versioni "old-school" me RxJS:

@Component({ /* ... */ })
export class ProductDashboardComponent implements OnInit, OnDestroy {
  private readonly products$ = new BehaviorSubject<Product[]>([]);
  private readonly filter$ = new BehaviorSubject<string>('');
  private readonly destroy$ = new Subject<void>();

  visible: Product[] = [];
  count = 0;

  constructor(private api: ProductApi) {}

  ngOnInit(): void {
    this.api.list().pipe(takeUntil(this.destroy$))
      .subscribe(list => this.products$.next(list));

    combineLatest([this.products$, this.filter$.pipe(debounceTime(300))])
      .pipe(
        map(([list, term]) =>
          list.filter(p => p.name.toLowerCase().includes(term.toLowerCase()))),
        takeUntil(this.destroy$),
      )
      .subscribe(visible => {
        this.visible = visible;
        this.count = visible.length;
      });
  }

  onFilter(term: string): void { this.filter$.next(term); }

  ngOnDestroy(): void {
    this.destroy$.next();
    this.destroy$.complete();
  }
}

Funksionon, por ka defektet tipike të kësaj qasjeje:

  • Shumë kod rrethues: dy subject-e, një subject shkatërrimi, dy subscription-e dhe një ngOnDestroy.
  • Rrjedhë e vështirë për t'u ndjekur: për të kuptuar nga vjen count duhet lexuar i gjithë zinxhiri i operatorëve.
  • Rreziku i memory leak: mjafton të harrosh një takeUntil që një subscription të mbetet gjallë pas shkatërrimit të komponentit.
  • Gjendje e dyfishuar: visible dhe count janë kopje që duhen mbajtur të sinkronizuara me dorë.

Hap pas hapi: gjendja me Signals

Hapi 1: vlerat dhe input-et bëhen signal

Çdo BehaviorSubject që përfaqëson një gjendje bëhet një writable signal. Përditësohet me .set() kur zëvendësohet vlera dhe me .update() kur vlera e re varet nga ajo e mëparshmja:

readonly products = signal<Product[]>([]);
readonly filter = signal('');

onFilter(term: string): void {
  this.filter.set(term);                         // replace the value
}

addProduct(p: Product): void {
  this.products.update(list => [...list, p]);    // derive from the previous one
}

Hapi 2: logjika e derivuar bëhet computed

combineLatest-i me map bëhet një computed(). Varësitë nuk deklarohen: Angular i zbulon duke gjurmuar cilët signal-e lexohen. Për më tepër, një computed është dembel dhe i memorizuar: rillogaritet vetëm kur ndryshon një varësi, dhe vetëm nëse dikush e lexon.

readonly visible = computed(() => {
  const term = this.filter().toLowerCase();
  return this.products().filter(p => p.name.toLowerCase().includes(term));
});

readonly count = computed(() => this.visible().length);

// Template: no async pipe, no subscriptions
// @for (p of visible(); track p.id) { <app-product-card [product]="p" /> }
// <span>{{ count() }} products</span>

visible dhe count nuk janë më kopje për t'u sinkronizuar: janë gjithmonë koherente nga vetë ndërtimi.

Hapi 3: efektet anësore, me effect

effect() shërben për të komunikuar me botën e jashtme: ruajtje në localStorage, shkrim i një log-u, sinkronizim i një librarie të palëve të treta si një grafik ose një hartë. Nuk shërben, përkundrazi, për të mbajtur të rreshtuar dy signal-e: në atë rast përgjigjja është pothuajse gjithmonë një computed.

constructor() {
  // OK: a side effect towards the outside world
  effect(() => localStorage.setItem('dashboard.filter', this.filter()));
}

// Anti-pattern: an effect that copies one signal into another
effect(() => this.total.set(this.visible().length));

// Better: derive it, and let Angular keep it in sync
readonly total = computed(() => this.visible().length);

Ura mes dy botëve: rxjs-interop

Thirrjet drejt backend-it — një API REST ose NestJS — mbeten natyrshëm Observable: HttpClient kthen stream-e, dhe aty duhen anulimi, ripërpjekjet dhe operatorët kohorë. Paketa @angular/core/rxjs-interop ofron dy ura:

  • toSignal(): e shndërron një stream, për shembull një përgjigje HTTP, në një signal për t'u përdorur drejtpërdrejt në template.
  • toObservable(): e shndërron një signal në stream kur duhen operatorë kohorë si debounceTime.

Ja një kërkim në kohë reale që i përdor të dyja: fusha e kërkimit është një signal, debounce-i dhe anulimi i kërkesave mbeten te RxJS, dhe rezultati kthehet te template-i si signal. Të dyja funksionet duhen thirrur në një kontekst injection, për shembull si inicializues fushash.

private readonly api = inject(ProductApi);

readonly query = signal('');

// signal → Observable (time operators) → signal (template)
readonly results = toSignal(
  toObservable(this.query).pipe(
    debounceTime(300),
    distinctUntilChanged(),
    switchMap(q => this.api.search(q)),
  ),
  { initialValue: [] as Product[] },
);

// Template: <input (input)="query.set($any($event.target).value)" />
//           @for (p of results(); track p.id) { ... }

Model arkitekturor: një store i bazuar te Signals

Kur gjendja u duhet disa komponentëve, logjika zhvendoset në një service. Rregulli është enkapsulimi: signal-i i shkrueshëm mbetet privat, jashtë ekspozohet një version vetëm për lexim me .asReadonly(), dhe ndryshimet kalojnë vetëm përmes metodave të service-it.

@Injectable({ providedIn: 'root' })
export class CartStore {
  private readonly _items = signal<CartItem[]>([]);

  readonly items = this._items.asReadonly();   // read-only from outside
  readonly total = computed(() =>
    this._items().reduce((sum, i) => sum + i.price * i.qty, 0));

  add(item: CartItem): void { this._items.update(list => [...list, item]); }
  clear(): void { this._items.set([]); }
}

Për aplikacione më të mëdha, Signal Store i @ngrx/signals e sjell të njëjtin model në nivel global, me një strukturë standarde për gjendjen, vlerat e derivuara dhe metodat.

Përfundime dhe listë kontrolli

  1. Përdor Signals për gjendjen sinkrone dhe UI-në: vlera aktuale, filtra, përzgjedhje, numërime.
  2. Përdor RxJS për ngjarjet asinkrone dhe rrjedhat në kohë: HTTP, WebSocket, debounce, anulim. Operatorët RxJS mbeten të çmuar.
  3. Përdor computed për gjithçka që derivohet, kurrë një effect që kopjon vlera.
  4. Bëji dy botët të bashkëpunojnë me rxjs-interop, në vend që të rishkruash gjithçka.
  5. Migro një komponent në një kohë, duke nisur nga ata me më shumë subscription-e manuale.

E ke migruar tashmë një pjesë të aplikacionit tënd te Signals, apo ke dyshime se ku ta vendosësh kufirin me RxJS? Lër një koment më poshtë me përvojën tënde: pyetjet më interesante do të bëhen ide për artikujt e ardhshëm.

💬 Shënime nga lexuesit

0 shënime

Shkruaj një shënim

Ndaj mendimin tënd, një sugjerim ose një kompliment

Shënimet e fundit

Ende asnjë shënim. Bëhu i pari që komenton!