# 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.
- Published: 2026-07-02
- Updated: 2026-07-07
- Author: SharpSkill
- Tags: angular, rxjs, signals, observables, typescript, deep-dive
- Reading time: 10 min
---
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](https://angular.dev/guide/signals/rxjs-interop) traktuje oba modele jako komplementarne, a nie konkurencyjne, a [dokumentacja RxJS](https://rxjs.dev/guide/overview) 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.
```typescript
// search.component.ts
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: ``,
})
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](/technologies/angular/interview-questions/rxjs-operators) ć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.
```typescript
// cart.service.ts
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([]);
// 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.
```typescript
// dashboard.component.ts
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](/technologies/angular/interview-questions/angular-signals) dogłębnie omawia model reaktywności, a [przegląd funkcji Signals w Angularze 18](/blog/angular/angular-18-signals-new-features) pokazuje, jak ewoluowały te prymitywy.
## 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.
```typescript
// products.component.ts
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(`/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](https://v20.angular.dev/api/core/rxjs-interop/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`.
```typescript
// live-feed.component.ts
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](/technologies/angular) grupuje moduły o RxJS, Signals i detekcji zmian, które bezpośrednio odpowiadają tym pytaniom.
## 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.
---
Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack.
HTML version of this page: https://sharpskill.dev/pl/blog/angular/rxjs-angular-operators-subjects-signals-interop