Współdziałanie sygnałów i RxJS w Angularze na praktycznym przykładzie
Sygnały w Angularze to nowy reactive primitive, który usprawni sposób w jaki tworzymy aplikacje w angularze ora zpoprawi Developer Experience. Znacząco również wpłynie na mechanizm detekcji zmian.
W tym artykule wyjaśnię, jak utworzyć komponent oparty na sygnałach. Dzięki praktycznemu przykładowi dowiesz się:
- Jak przekształcić RxJS observable na sygnał
- Jak przekształcić sygnał na observable
Wszyscy lubimy wizualizacje, a więc zobaczmy, co zamierzamy stworzyć.
Poniższy kod jest naszym punktem wyjścia. Usprawnimy go wprowadzając koncepcję
sygnałów.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<div class="page-container"> <mat-form-field class="page-container--form-field"> <mat-label>Enter User Id (Empty will fetch all)</mat-label> <input matInput type="text" [matAutocomplete]="autoComplete" /> <mat-spinner *ngIf="false" matSuffix class="page-container--spinner" ></mat-spinner> <mat-autocomplete #autoComplete="matAutocomplete"> <mat-option> Option 1 </mat-option> <mat-option> Option 2 </mat-option> <mat-option> Option 3 </mat-option> </mat-autocomplete> </mat-form-field> </div> |
Konwersja z RxJS observable na sygnał
Zacznijmy od pobrania danych z serwisu, przekształcenia go do sygnału i przeiterowania
przez elementy tworzące opcje autocomplete’a.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
import { HttpClient } from '@angular/common/http'; import { Injectable, inject } from '@angular/core'; import { Observable, delay, throwError } from 'rxjs'; import { Post } from './post.type'; @Injectable({ providedIn: 'root', }) export class PostsService { private http = inject(HttpClient); get(userId?: number): Observable<Post[]> { if (userId == 100) { return throwError(() => new Error('User not found')); } return this.http .get<Post[]>('https://jsonplaceholder.typicode.com/posts', { params: { ...(userId ? { userId: userId.toString() } : {}), }, }) .pipe(delay(2000)); } } |
Metoda get()
:
- Przyjmuje userId jako argument
- Wyrzuca błąd jeżeli userId wynosi 100
(to jest tylko manualny sposób na rzucenie błędu abyśmy mogli zobaczyć jak zaaplikować podstawową obsługę błędów) - Stosuje sztuczny delay, aby umożliwić wyświetlenie loader’a
Tak jak wspomniałem powyżej, przejdziemy przez to wszystko krok po kroku.
W poniższym kodzie, wykorzystujemy metodę toSignal
, która przyjmuje observable i zwraca
sygnał oparty na wartościach otrzymanych z observable. Warto zaznaczyć, że subskrypcja do observable zarządzania
jest automatyczna, oraz jest czyszczona kiedy injection context jest niszczony.
1 2 3 4 5 |
@Component({...}) export class PostsComponent { private postsService = inject(PostsService); posts = toSignal(this.postsService.get()); } |
Przeiterujmy teraz po obiektach Post w szablonie HTML. Zauważ, że musimy użyć nawiasów,
aby uzyskać wartości z sygnału. Może się to wydawać złym podejściem gdyż uczyliśmy się, że metody w szablonie
wywoływane są przy każdym cyklu detekcji zmian, jednakże nie dotyczy to sygnałów.
1 2 3 4 5 |
<mat-autocomplete #autoComplete="matAutocomplete"> <mat-option *ngFor="let post of posts()" [value]="post.title"> {{ post.title }} </mat-option> </mat-autocomplete> |
Jak dotąd udało nam się wykonać pierwszy krok wykorzystując jedynie metodę toSignal
. Spróbujmy teraz uzyskać dane wejściowe użytkownika i przesłać je do serwisu.
Konwersja sygnału na observable
Powiązanie Binding do inputa również będzie oparty na sygnałach. Nazwiemy go userId
, którego
wartość początkowa wynosić będzie undefined
.
1 2 3 4 5 |
export class PostsComponent { private postsService = inject(PostsService); userId = signal<number | undefined>(undefined); posts = toSignal(this.postsService.get()); } |
Musimy zastosować two-way data binding, ale jako że jeszcze nie możemy wykorzystać jego
składni, oddzielimy property binding wykorzystując event binding. (Podczas pisania artykułu wersja Angulara wynosi 16.0.4)
1 2 3 4 5 6 7 |
<input [ngModel]="userId()" (ngModelChange)="userId.set($event)" matInput type="text" [matAutocomplete]="autoComplete" /> |
Aby wysłać żądanie HTTP za każdym razem, kiedy wartość userID się zmieni, możemy wykorzystać kombinacje efektu oraz BehaviourSubject
, tak jak widać poniżej.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
import { effect, signal } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { BehaviorSubject } from 'rxjs'; @Component({...}) export class PostsComponent { private postsService = inject(PostsService); userId = signal<number | undefined>(undefined); userId$ = new BehaviorSubject<number | undefined>(undefined); constructor() { this.userId$ .pipe( // Do something here, takeUntilDestroyed() ) .subscribe(); effect(() => { this.userId$.next(this.userId()); }); } } |
Podejście to działa prawidłowo, jednak wymaga od dewelopera zarządzania subskrypcją.
Wydaje się jednak, że jest to wzorzec, którego możemy użyć za każdym razem, gdy łączymy sygnał z niektórymi operatorami RxJS. Z tego względu zespół Angulara stworzył metodę toObservable
, która przekształca sygnał do observable oraz automatycznie zarządza jego subskrypcją. Dzięki tej metodzie, kod znacząco się skróci.
1 2 3 4 5 6 7 8 |
export class PostsComponent { private postsService = inject(PostsService); userId = signal<number | undefined>(undefined); private posts$ = toObservable(this.userId).pipe( switchMap((userId) => this.postsService.get(userId)) ); posts = toSignal(this.posts$); } |
W powyższym kodzie mamy pole posts$
, który ma bardzo krótki cykl życia, gdyż jest przekształcany na sygnał za pomocą metody toSignal
. Poprawmy to trochę i użyjmy operatora debounceTime
.
1 2 3 4 5 6 7 8 9 10 |
export class PostsComponent { private postsService = inject(PostsService); userId = signal<number | undefined>(undefined); posts = toSignal( toObservable(this.userId).pipe( debounceTime(500), switchMap((userId) => this.postsService.get(userId)) ) ); } |
Jako że przekonwertowaliśmy sygnał do observable, możemy zastosować operatory RxJS do obsługi stanu ładowania wraz z obsługą błędów. Stan ładowania obsługujemy za pomocą sygnału isLoading signal<boolean>.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
export class PostsComponent { private postsService = inject(PostsService); isLoading = signal<boolean>(false); userId = signal<number | undefined>(undefined); posts = toSignal( toObservable(this.userId).pipe( debounceTime(500), tap(() => this.isLoading.set(true)), switchMap((userId) => this.postsService.get(userId).pipe(catchError(() => of([]))) ), tap(() => this.isLoading.set(false)) ) ); } |
I to wszystko!
Na koniec podzielę się kilkoma sugestiami:
- Postaraj się nie wykorzystywać async pipe’a w szablonie HTML, zwiększy to ilość cykli detekcji zmian
- Spróbuj przekształcić stan twojego komponentu do sygnału
- Nie obawiaj się korzystania z operatorów RxJS
- Postaraj się wykorzystywać efekty w celu tworzenia logów bądź przy manipulacji DOM.
Możesz również obejrzeć film opisujący przedstawiony przykład na moim kanałe Youtube:
Learn Angular Signals RxJS Interop From a Practical Example
Dziękuję za przeczytanie mojego artykułu.
Dodaj komentarz