Kurs Angular · Moduł 7: RxJS

Praktyczne Wzorce RxJS w Angular

2 min czytania ·
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?

PrzypadekSignalsRxJS
Stan komponentutaknie
Proste obliczenia pochodnetak, computed()nie
HTTP requestsnietak
Debounce/throttlenietak
Wielokrotne transformacjenietak
Łączenie strumieninietak
// 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. 1. Kiedy forkJoin emituje wartość?

  2. 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)

Przydatne artykuły