RxJS w Angularze 2026: operatory, Subjecty i interop z Signals

RxJS w Angularze 2026: opanuj operatory, Subjecty i wzorce interoperacyjności z Signals używane na produkcji oraz pytania rekrutacyjne, które padają najczęściej.

RxJS w Angularze 2026 operatory, Subjecty i interoperacyjność z Signals

RxJS w Angularze pozostaje fundamentem programowania asynchronicznego w 2026 roku, mimo że Signals zmieniają sposób, w jaki komponenty Angulara śledzą stan synchroniczny. Umiejętność rozpoznania, kiedy sięgnąć po Observable, kiedy lepiej sprawdzi się Signal i jak jedno współgra z drugim, stała się podstawową kompetencją każdego dewelopera Angulara. Ten szczegółowy przewodnik prowadzi przez operatory, Subjecty i wzorce interoperacyjności, które odróżniają pewną odpowiedź na rozmowie kwalifikacyjnej od takiej, która grzęźnie w miejscu.

Signals kontra Observables w jednym zdaniu

Signals modelują stan synchroniczny, który zawsze ma bieżącą wartość; Observables modelują asynchroniczne strumienie zdarzeń rozłożone w czasie. Angular 20 zachowuje oba modele, a pakiet @angular/core/rxjs-interop jest oficjalnym mostem między nimi.

Dlaczego RxJS wciąż ma znaczenie obok Angular Signals

Wprowadzenie Signals skłoniło wiele zespołów do pytania, czy RxJS odchodzi do lamusa. Odpowiedź, trzy główne wersje później, brzmi: nie. Signals doskonale reprezentują stan, który ma wartość tu i teraz: licznik, pole formularza, wybraną zakładkę. Observables doskonale modelują zdarzenia, które napływają w czasie i mogą wymagać transformacji, anulowania lub łączenia: odpowiedzi HTTP, wiadomości WebSocket, wejście z klawiatury, zdarzenia routera i timery.

Same API Angulara pokazują ten podział. HttpClient nadal zwraca Observables. Formularze reaktywne udostępniają valueChanges i statusChanges jako Observables. Zdarzenia routera płyną strumieniem przez Observable. Wszędzie tam, gdzie przez ten sam kanał przepływa wiele zdarzeń asynchronicznych, operatory RxJS pozostają najbardziej ekspresyjnym dostępnym narzędziem. Oficjalny przewodnik po reaktywności Angulara traktuje oba modele jako komplementarne, a nie konkurencyjne, a dokumentacja RxJS pozostaje obowiązkową lekturą przy każdym nietrywialnym przepływie danych.

Praktyczna zasada, która broni się w code review: używaj Signals dla lokalnego stanu komponentu odczytywanego w szablonach, a Observables dla asynchronicznych potoków wymagających operatorów. Warstwa interoperacyjności łączy oba modele, więc żaden z nich nie staje się murem nie do przejścia.

Operatory łączące potwierdzają ten wniosek. Koordynacja kilku źródeł asynchronicznych, jak scalanie parametrów trasy z zapisanymi filtrami użytkownika i strumieniem cen na żywo, to dokładnie to, do czego stworzono combineLatest, withLatestFrom i forkJoin. Signals potrafią wyprowadzać wartości obliczane z innych sygnałów, ale nie wyrażą koordynacji opartej na czasie w rodzaju „emituj dopiero, gdy wszystkie trzy źródła wyprodukują co najmniej jedną wartość". Ta deklaratywna kompozycja rozłożona w czasie to trwały powód, dla którego RxJS pozostaje w zestawie narzędzi.

Kluczowe operatory RxJS, na których polega każdy deweloper Angulara

Operatory to czyste funkcje, które przyjmują Observable i zwracają nowy. Klasycznym scenariuszem z rozmów kwalifikacyjnych jest wyszukiwanie w trakcie pisania, ponieważ uruchamia cztery operatory naraz: ogranicza częstotliwość wejścia, pomija duplikaty, anuluje nieaktualne żądania i odzyskuje sprawność po błędach.

search.component.tstypescript
import { Component, inject } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { HttpClient } from '@angular/common/http';
import { debounceTime, distinctUntilChanged, switchMap, catchError } from 'rxjs/operators';
import { of } from 'rxjs';

@Component({
  selector: 'app-search',
  imports: [ReactiveFormsModule],
  template: `<input [formControl]="query" placeholder="Search repositories" />`,
})
export class SearchComponent {
  private http = inject(HttpClient);
  query = new FormControl('');

  results$ = this.query.valueChanges.pipe(
    debounceTime(300),               // wait until typing pauses for 300ms
    distinctUntilChanged(),          // ignore emissions that repeat the last value
    switchMap((term) =>              // cancel the in-flight request, start a fresh one
      this.http.get(`/api/search?q=${term}`).pipe(
        catchError(() => of([]))     // recover from a failed request without killing the stream
      )
    )
  );
}

Subtelny szczegół, który badają rekruterzy, to dlaczego switchMap znajduje się na poziomie zewnętrznym, podczas gdy catchError jest zagnieżdżony wewnątrz niego. Umieszczenie catchError na wewnętrznym Observable HTTP sprawia, że nieudane żądanie zwraca pustą tablicę, a strumień zewnętrzny płynie dalej. Przeniesienie catchError do potoku zewnętrznego zakończyłoby całe wyszukiwanie po pierwszym błędzie, psując każde kolejne naciśnięcie klawisza. Cztery operatory spłaszczające odpowiadają, każdy na inne pytanie o współbieżność, a wybór niewłaściwego jest jednym z najczęstszych źródeł sytuacji wyścigu w aplikacjach Angulara.

Wybór operatora spłaszczającego

Użyj switchMap, aby anulować poprzednie żądanie wewnętrzne i zachować tylko najnowsze (wyszukiwanie typu typeahead). Użyj mergeMap, aby uruchomić każde żądanie wewnętrzne współbieżnie (niezależne równoległe przesyłanie plików). Użyj concatMap, aby ustawić żądania w ścisłej kolejności (sekwencyjne zapisy, które nie mogą się nakładać). Użyj exhaustMap, aby ignorować nowe wejścia, dopóki bieżące się nie zakończy (blokowanie dwukrotnie klikniętego przycisku wysyłania).

Właściwy wybór w tym miejscu zamienia narowistą funkcję w przewidywalną. Moduł operatorów RxJS w SharpSkill ćwiczy te rozróżnienia na ocenianych pytaniach, które odtwarzają dokładnie te sytuacje wyścigu, którym zapobiega każdy operator.

Subjecty i multicasting: BehaviorSubject, ReplaySubject i Subject

Zwykły Observable jest unicastowy: każdy subskrybent wyzwala nowe wykonanie. Subject jest jednocześnie Observable i Observerem, co czyni go multicastowym. Ta właściwość sprawia, że Subjecty są standardowym sposobem budowania lekkiego magazynu stanu lub szyny zdarzeń wewnątrz serwisu.

Trzy warianty różnią się tym, co odtwarzają spóźnionym subskrybentom. Zwykły Subject nie emituje niczego z przeszłości, więc subskrybent widzi tylko wartości, które napłyną po jego subskrypcji. BehaviorSubject przechowuje bieżącą wartość i natychmiast ją odtwarza, dlatego wymaga wartości początkowej. ReplaySubject buforuje konfigurowalną liczbę wcześniejszych emisji i odtwarza je wszystkie.

cart.service.tstypescript
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class CartService {
  // BehaviorSubject holds the latest value and replays it to new subscribers
  private readonly itemsSubject = new BehaviorSubject<string[]>([]);

  // expose a read-only stream, never the writable Subject itself
  readonly items$ = this.itemsSubject.asObservable();

  add(item: string): void {
    const current = this.itemsSubject.value;    // synchronous access to the last value
    this.itemsSubject.next([...current, item]); // emit the next immutable state
  }
}

Liczą się tu dwa nawyki. Po pierwsze, udostępnianie asObservable() zamiast samego Subjecta uniemożliwia zewnętrznemu kodowi wywołanie next() i modyfikowanie stanu tylnymi drzwiami. Po drugie, emitowanie nowej referencji tablicy zamiast modyfikacji w miejscu utrzymuje przewidywalność detekcji zmian i porównań równości. Ten wzorzec wyprzedza Signals i wciąż dobrze się sprawdza dla współdzielonego, wstrzykiwalnego stanu, z którego korzysta wiele niepowiązanych komponentów.

Mimo to BehaviorSubject używany wyłącznie do przechowywania pojedynczej bieżącej wartości odczytywanej w szablonach często lepiej wyrazić dziś jako zapisywalny Signal, co eliminuje subskrypcję i ceremoniał asObservable(). Subject pozostaje mocniejszym wyborem, gdy magazyn musi też transformować, ograniczać częstotliwość lub łączyć swoje emisje z innymi strumieniami, ponieważ te operatory nie mają bezpośredniego odpowiednika wśród Signals. Rozstrzyganie między nimi zależnie od przypadku użycia, zamiast domyślnego stosowania jednego wszędzie, to oznaka biegłości, którą rekruterzy nagradzają.

Interoperacyjność RxJS z Signals przy użyciu toSignal i toObservable

Pakiet interoperacyjności dostarcza dwie sztandarowe funkcje. toSignal konwertuje Observable na Signal tylko do odczytu, który szablon może konsumować bezpośrednio, i automatycznie zarządza cyklem życia subskrypcji. toObservable robi odwrotnie, zamieniając Signal na Observable, aby operatory RxJS mogły przetwarzać jego zmiany.

dashboard.component.tstypescript
import { Component, signal, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { toSignal, toObservable } from '@angular/core/rxjs-interop';
import { switchMap } from 'rxjs/operators';

@Component({ selector: 'app-dashboard', template: `{{ user()?.name }}` })
export class DashboardComponent {
  private http = inject(HttpClient);

  // a Signal drives the query; toObservable turns each change into a stream event
  readonly userId = signal(1);
  private readonly user$ = toObservable(this.userId).pipe(
    switchMap((id) => this.http.get<{ name: string }>(`/api/users/${id}`))
  );

  // toSignal subscribes immediately and unsubscribes on destroy, no manual teardown
  readonly user = toSignal(this.user$, { initialValue: null });
}

Opcja initialValue zasługuje na uwagę. Bez niej toSignal zwraca Signal typowany jako wartość lub undefined, ponieważ nic jeszcze nie wyemitowano. Przekazanie initialValue: null daje szablonowi zdefiniowany stan startowy i czystszy typ. Dla źródeł synchronicznych, takich jak BehaviorSubject, requireSync: true gwarantuje dostępność wartości przy pierwszym odczycie i całkowicie usuwa undefined z typu. Ten dwukierunkowy most pozwala bazie kodu przyjmować Signals stopniowo, bez przepisywania każdego istniejącego potoku Observable. Towarzyszący moduł rozmów kwalifikacyjnych o Angular Signals dogłębnie omawia model reaktywności, a przegląd funkcji Signals w Angularze 18 pokazuje, jak ewoluowały te prymitywy.

Gotowy na rozmowy o Angular?

Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.

Mostkowanie Observables do zasobów sygnałowych za pomocą rxResource

Angular 20 promuje rxResource jako deklaratywny sposób ładowania danych asynchronicznych z Observable do stanu opartego na sygnałach. Śledzi stan ładowania, błędu i rozwiązania jako sygnały, co usuwa większość ręcznego księgowania subskrypcji, którego dawniej wymagały wywołania HTTP.

products.component.tstypescript
import { Component, signal, inject } from '@angular/core';
import { rxResource } from '@angular/core/rxjs-interop';
import { HttpClient } from '@angular/common/http';

@Component({ selector: 'app-products', template: `` })
export class ProductsComponent {
  private http = inject(HttpClient);
  readonly category = signal('laptops');

  // Angular 20 renamed the fields: request -> params and loader -> stream
  readonly products = rxResource({
    params: () => ({ category: this.category() }),
    stream: ({ params }) =>
      this.http.get<Product[]>(`/api/products?category=${params.category}`),
  });
  // products.value(), products.isLoading() and products.error() are all signals
}

Zmiana nazw z request/loader w Angularze 19 na params/stream w Angularze 20 często stanowi pułapkę, więc wspomnienie aktualnych nazw pól świadczy o aktualnej wiedzy podczas rozmowy. Gdy sygnał category się zmienia, rxResource anuluje poprzednie żądanie i uruchamia nowy strumień, dając semantykę switchMap za darmo. Dokumentacja API rxResource opisuje pełny cykl życia statusu.

Unikanie wycieków pamięci dzięki takeUntilDestroyed

Każde ręczne subscribe(), które przeżywa swój komponent, powoduje wyciek pamięci i może wywoływać callbacki na zniszczonym widoku. Historycznym rozwiązaniem był wzorzec takeUntil(this.destroy$) z Subjectem kończonym w ngOnDestroy. Angular zastąpił ten boilerplate operatorem takeUntilDestroyed, który wiąże subskrypcję z bieżącym kontekstem wstrzykiwania lub jawnym DestroyRef.

live-feed.component.tstypescript
import { Component, inject, DestroyRef } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { interval } from 'rxjs';

@Component({ selector: 'app-live-feed', template: `` })
export class LiveFeedComponent {
  private destroyRef = inject(DestroyRef);

  constructor() {
    interval(1000)
      // completes the subscription automatically when the component is destroyed
      .pipe(takeUntilDestroyed(this.destroyRef))
      .subscribe((tick) => console.log('tick', tick));
  }
}

Wywoływany wewnątrz konstruktora lub inicjalizatora pola, takeUntilDestroyed() nie potrzebuje argumentu, ponieważ odczytuje otaczający DestroyRef. Wywoływany gdziekolwiek indziej, na przykład wewnątrz metody, wymaga jawnie przekazanego DestroyRef, jak pokazano powyżej. Preferowanie toSignal i rxResource całkowicie eliminuje ręczne subskrypcje, co jest najczystszym sposobem uniknięcia wycieków; gdy surowa subskrypcja jest naprawdę potrzebna, takeUntilDestroyed jest właściwym zabezpieczeniem.

Pułapka kontekstu wstrzykiwania

Wywołanie takeUntilDestroyed() bez argumentu poza kontekstem wstrzykiwania rzuca błąd wykonania. Subskrybowanie wewnątrz obsługi zdarzenia, setTimeout lub callbacku RxJS znajduje się poza tym kontekstem, więc DestroyRef trzeba przechwycić wcześniej (zwykle jako wstrzyknięte pole) i przekazać jawnie. Zapomnienie o tym jest najczęstszą przyczyną błędu „takeUntilDestroyed() can only be used within an injection context".

Pytania rekrutacyjne o RxJS w Angularze warte przećwiczenia

Rekruterzy zwykle krążą wokół tych samych tematów o wysokiej wartości sygnałowej. Umiejętność zwięzłej odpowiedzi na nie oddziela kandydatów, którzy używali RxJS, od tych, którzy jedynie o nim czytali.

  • Hot kontra cold Observables. Cold Observable uruchamia swojego producenta przy każdej subskrypcji, więc każdy subskrybent otrzymuje niezależne wykonanie (świeże żądanie HTTP). Hot Observable współdzieli jednego producenta między subskrybentami, co tworzą właśnie Subjecty i share().
  • switchMap kontra mergeMap kontra concatMap kontra exhaustMap. switchMap anuluje poprzedni wewnętrzny Observable (idealny do wyszukiwania). mergeMap uruchamia wszystkie wewnętrzne Observables współbieżnie. concatMap ustawia je w kolejności. exhaustMap ignoruje nowe wejścia, gdy jedno jest w toku (idealny do zapobiegania podwójnemu wysyłaniu formularzy).
  • Jak zapobiegać wyciekom pamięci. Preferuj toSignal, rxResource lub pipe async, które wszystkie odsubskrybowują automatycznie; dla ręcznych subskrypcji używaj takeUntilDestroyed.
  • Kiedy konwertować Observable na Signal. Sięgnij po toSignal, gdy szablon potrzebuje wartości synchronicznie, a źródło jest asynchroniczne, pozwalając detekcji zmian odczytać najnowszą wartość bez pipe async.

Przerabianie ocenianych wersji tych pytań pod presją czasu to najszybszy sposób na ich przyswojenie. Centrum technologii Angular grupuje moduły o RxJS, Signals i detekcji zmian, które bezpośrednio odpowiadają tym pytaniom.

Zacznij ćwiczyć!

Sprawdź swoją wiedzę z naszymi symulatorami rozmów i testami technicznymi.

Podsumowanie

RxJS i Signals są partnerami w nowoczesnym stosie Angulara, nie rywalami. Wnioski warte zabrania do następnego projektu lub na rozmowę:

  • Używaj Signals dla synchronicznego stanu komponentu i Observables dla asynchronicznych potoków zdarzeń wymagających operatorów.
  • Sięgaj po switchMap, aby anulować nieaktualną pracę, exhaustMap, aby blokować duplikaty, i trzymaj catchError na wewnętrznym Observable, aby strumień zewnętrzny przetrwał awarie.
  • Udostępniaj stan Subjecta przez asObservable() i emituj niemutowalne wartości, aby zachować przewidywalność detekcji zmian.
  • Łącz oba modele przez toSignal i toObservable, przekazując initialValue lub requireSync dla czystych typów.
  • Ładuj dane asynchroniczne deklaratywnie za pomocą rxResource w Angularze 20, pamiętając o nazwach pól params i stream.
  • Zabezpieczaj każdą pozostałą ręczną subskrypcję za pomocą takeUntilDestroyed, aby wyeliminować wycieki.

Tagi

#angular
#rxjs
#signals
#observables
#typescript
#deep-dive

Udostępnij

Powiązane artykuły