Kurs Angular · Moduł 7: RxJS
Praktyczne Wzorce RxJS w Angular
W tej lekcji5
Wyszukiwarka z debounce
@Component({
template: `
<input #searchInput placeholder="Szukaj samuraja...">
@for (result of searchResults$ | async; track result.id) {
<div>{{ result.name }}</div>
}
`
})
export class SearchComponent implements AfterViewInit {
@ViewChild('searchInput') searchInput!: ElementRef;
searchResults$!: Observable<Samurai[]>;
private http = inject(HttpClient);
ngAfterViewInit(): void {
this.searchResults$ = fromEvent<InputEvent>(
this.searchInput.nativeElement,
'input'
).pipe(
map(event => (event.target as HTMLInputElement).value),
debounceTime(300), // Czekaj 300ms
distinctUntilChanged(), // Ignoruj te same wartości
filter(query => query.length >= 2), // Min 2 znaki
switchMap(query => // Anuluj poprzednie zapytanie
this.http.get<Samurai[]>(`/api/search?q=${query}`).pipe(
catchError(() => of([])) // Obsługa błędów
)
)
);
}
}Polling z retry
@Injectable({ providedIn: 'root' })
export class StatusService {
private http = inject(HttpClient);
getStatusWithPolling(): Observable<Status> {
return interval(5000).pipe( // Co 5 sekund
startWith(0), // Natychmiast na start
switchMap(() =>
this.http.get<Status>('/api/status').pipe(
retry({
count: 3,
delay: 1000 // Ponów 3 razy co sekundę
}),
catchError(error => {
console.error('Status check failed:', error);
return of({ status: 'unknown', error: true });
})
)
),
shareReplay(1) // Dziel wynik między subskrybentami
);
}
}Ładowanie z cache
@Injectable({ providedIn: 'root' })
export class CachedDataService {
private http = inject(HttpClient);
private cache$ = new Map<string, Observable<any>>();
getData<T>(url: string): Observable<T> {
if (!this.cache$.has(url)) {
this.cache$.set(url,
this.http.get<T>(url).pipe(
shareReplay({ bufferSize: 1, refCount: true }),
tap({
error: () => this.cache$.delete(url)
})
)
);
}
return this.cache$.get(url)!;
}
invalidateCache(url: string): void {
this.cache$.delete(url);
}
}Obsługa wielu żądań zależnych
getSamuraiWithWeapons(id: number): Observable<SamuraiWithWeapons> {
return this.http.get<Samurai>(`/api/samurai/${id}`).pipe(
switchMap(samurai =>
forkJoin({
weapons: this.http.get<Weapon[]>(`/api/samurai/${id}/weapons`),
skills: this.http.get<Skill[]>(`/api/samurai/${id}/skills`)
}).pipe(
map(({ weapons, skills }) => ({
...samurai,
weapons,
skills
}))
)
)
);
}Signals vs RxJS - Kiedy co?
| Przypadek | Signals | RxJS |
|---|---|---|
| Stan komponentu | tak | nie |
| Proste obliczenia pochodne | tak, computed() | nie |
| HTTP requests | nie | tak |
| Debounce/throttle | nie | tak |
| Wielokrotne transformacje | nie | tak |
| Łączenie strumieni | nie | tak |
// Integracja - najlepsze z obu światów
@Component({...})
export class HybridComponent {
private http = inject(HttpClient);
// Signal dla stanu UI
isLoading = signal(false);
selectedId = signal<number | null>(null);
// RxJS dla HTTP
samurai$ = toObservable(this.selectedId).pipe(
filter(id => id !== null),
tap(() => this.isLoading.set(true)),
switchMap(id => this.http.get<Samurai>(`/api/samurai/${id}`)),
tap(() => this.isLoading.set(false))
);
// Observable → Signal
samurai = toSignal(this.samurai$);
}Hasła z dokumentacji w tej lekcji
Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Kiedy forkJoin emituje wartość?
2. Do czego służy operator shareReplay() w praktycznych wzorcach RxJS?
To 2 z 4 pytań do tej lekcji. Pozostałe rozwiążesz w grze.
Zadania praktyczne w grze
- Edytor kodu
Uzupełnij kod, który wysyła trzy zapytania HTTP równolegle i reaguje dopiero wtedy, gdy wszystkie się zakończą.
- Układanie w pionie
Ułóż operatory pełnego łańcucha wyszukiwarki z debounce w kolejności wewnątrz pipe():
- Układanie w poziomie
Ułóż konwersję Observable na Signal: samurai = toSignal(this.samurai$)
- Klikanie w kolejności
Ułóż wywołanie konwertujące Signal na Observable: toObservable(this.selectedId)