Subject w RxJS rozwiązuje bardzo konkretny problem: pozwala podać te same wartości do wielu subskrybentów, zamiast uruchamiać osobny strumień dla każdego z nich. W tym tekście pokazuję, czym różni się od zwykłego Observable, jak działa multicast, jakie są jego warianty i kiedy lepiej wybrać `share()` albo `shareReplay()`. Dorzucam też praktyczne przykłady w JavaScript, bo właśnie na kodzie najłatwiej zobaczyć, gdzie Subject pomaga, a gdzie zaczyna przeszkadzać.
Najważniejsze informacje o Subject w RxJS
- Subject łączy rolę Observable i Observer: można go subskrybować, ale też wysyłać do niego wartości przez `next()`.
- Jego główny sens to multicast, czyli rozsyłanie jednej emisji do wielu aktywnych subskrybentów.
- Zwykły Subject nie pamięta historii, więc nowi subskrybenci widzą tylko przyszłe wartości.
- Jeśli potrzebujesz wartości początkowej albo ostatniego stanu, zwykle lepszy będzie BehaviorSubject.
- Gdy chcesz tylko współdzielić jedno źródło danych, często wystarczy `share()` zamiast ręcznego Subjecta.
Czym jest Subject i czym różni się od zwykłego Observable
Najprościej ujmując: zwykły Observable opisuje przepływ danych, a Subject potrafi ten przepływ jednocześnie obserwować i zasilać. To dlatego Subject bywa porównywany do kontrolowanego EventEmittera, tylko że w świecie RxJS i z pełnym wsparciem dla reaktywnego stylu pracy. Dokumentacja RxJS opisuje go właśnie jako specjalny typ Observable, który rozsyła wartości do wielu obserwatorów.
Różnica między Subjectem a klasycznym Observable jest ważniejsza, niż wygląda na pierwszy rzut oka. Zwykły Observable jest zazwyczaj unicast, czyli każda subskrypcja uruchamia osobną instancję źródła. Subject działa inaczej: jedna emisja trafia do wszystkich aktywnych subskrybentów, więc mówimy o multicast. W praktyce ja traktuję Subject jako wspólny kanał zdarzeń, a nie jako magazyn danych. To od razu podpowiada, kiedy ma sens, a kiedy lepiej sięgnąć po prostsze rozwiązanie.
Ta różnica prowadzi naturalnie do pytania, jak wygląda to w kodzie, bo właśnie tam najłatwiej zobaczyć, po co Subject w ogóle istnieje.

Jak działa multicast w praktyce
Jeśli podłączysz kilku obserwatorów do jednego Subjecta, każdy z nich dostanie dokładnie te same wartości w tym samym czasie. To jest sedno całego mechanizmu. Jedna emisja, wielu odbiorców, bez ponownego uruchamiania źródła dla każdej subskrypcji.
import { Subject } from 'rxjs';
const subject = new Subject();
subject.subscribe(value => console.log('A:', value));
subject.subscribe(value => console.log('B:', value));
subject.next('pierwsza wartość');
subject.next('druga wartość');W tym przykładzie oba subskrybujące miejsca zobaczą te same komunikaty. To dobrze pokazuje, że Subject nie tworzy osobnych strumieni dla każdego odbiorcy, tylko rozsyła sygnał wspólnym kanałem. Gdybym zrobił to samo na zwykłym cold Observable, często dostałbym osobne wykonanie źródła dla każdego subskrybenta.
Jest też drugi detal, o którym początkujący często zapominają: zwykły Subject nie buforuje poprzednich emisji. Jeśli wywołam `next()` przed subskrypcją, nowy odbiorca tego nie zobaczy.
import { Subject } from 'rxjs';
const subject = new Subject();
subject.next('to przepadnie');
subject.subscribe(value => console.log(value));
subject.next('to już będzie widoczne');To właśnie odróżnia Subject od wariantów, które pamiętają stan albo historię. I dlatego kolejny krok to wybór właściwego typu, a nie bezrefleksyjne używanie jednego narzędzia do wszystkiego.
Jak wybrać właściwy wariant Subjecta
Jeśli wiesz już, że potrzebujesz współdzielonej emisji, następnym pytaniem jest: czy ma się liczyć tylko bieżący sygnał, ostatnia wartość, historia, czy dopiero wynik końcowy po zakończeniu strumienia. Tu różnice między wariantami są naprawdę praktyczne, nie kosmetyczne.
| Typ | Co pamięta | Kiedy się sprawdza | Na co uważać |
|---|---|---|---|
| Subject | Nic | Eventy, komendy, sygnały jednorazowe | Nowi subskrybenci nie dostaną przeszłych wartości |
| BehaviorSubject | Ostatnią wartość i wartość startową | Stan aplikacji, aktualny wybór, formularz | Wymaga wartości początkowej, więc nie zawsze pasuje |
| ReplaySubject | Ostatnie N wartości albo dane z okna czasu | Historia zdarzeń, późne subskrypcje, odtwarzanie emisji | Łatwo przesadzić z pamięcią i buforem |
| AsyncSubject | Tylko ostatnią wartość, ale dopiero po `complete()` | Procesy kończące się jednym wynikiem | Bez zakończenia strumienia nic nie wypuści |
Jeżeli potrzebujesz jedynie współdzielić jedno źródło, ale nie chcesz ręcznie wywoływać `next()`, często lepsze będzie `share()`. Gdy chcesz zachować ostatnią wartość dla późniejszych subskrybentów, rozważ `shareReplay()` albo ReplaySubject. Ja zwykle zaczynam od najprostszego wariantu i dopiero potem dokładam pamięć, jeśli naprawdę jest potrzebna.
Z tego miejsca już łatwo przejść do praktyki, bo sama znajomość wariantów nie wystarczy, jeśli Subject jest źle zamknięty lub zbyt szeroko wystawiony na zewnątrz.
Jak używać Subjecta w JavaScript bez robienia sobie bałaganu
Najzdrowszy wzorzec, jaki stosuję, to ukrycie Subjecta wewnątrz klasy albo modułu i wystawienie na zewnątrz tylko odczytu przez `asObservable()`. Dzięki temu kod z zewnątrz może subskrybować zmiany, ale nie może samemu wpychać wartości do środka. To drobny detal, który bardzo mocno poprawia kontrolę nad przepływem danych.
import { Subject } from 'rxjs';
class SearchModel {
#changes = new Subject();
get changes$() {
return this.#changes.asObservable();
}
setQuery(value) {
this.#changes.next(value);
}
destroy() {
this.#changes.complete();
}
}
const model = new SearchModel();
const sub = model.changes$.subscribe(value => {
console.log('Nowe zapytanie:', value);
});
model.setQuery('rxjs');
model.setQuery('subject');
sub.unsubscribe();
model.destroy();Ten układ robi dwie rzeczy naraz. Po pierwsze, porządkuje odpowiedzialności: zapis zostaje wewnątrz obiektu, a odczyt na zewnątrz. Po drugie, jasno pokazuje moment zakończenia strumienia przez `complete()`, co ma znaczenie, gdy kanał zdarzeń rzeczywiście przestaje być potrzebny. Jeśli buduję komponent, serwis albo klasę pomocniczą, to właśnie taki układ jest dla mnie najczytelniejszy.
Warto też pamiętać o cyklu życia subskrypcji. Subject sam w sobie nie rozwiązuje problemu wycieków pamięci, więc jeśli odbiorca przestaje być potrzebny, trzeba go odłączyć. To szczególnie ważne w interfejsach, gdzie komponenty pojawiają się i znikają.
Gdy te zasady są już na miejscu, łatwiej zobaczyć najczęstsze pułapki i sytuacje, w których Subject jest po prostu nadmiarowy.
Najczęstsze błędy i kiedy lepiej wybrać `share()`
Najwięcej problemów widzę nie w samym Subjectcie, tylko w tym, że staje się on uniwersalnym klejem do wszystkiego. To kuszące, bo działa szybko, ale po kilku tygodniach taki kod bywa trudny do śledzenia i testowania.
- Nie używaj Subjecta jako globalnego busa do wszystkiego. Im mniej granic, tym trudniej zrozumieć, skąd przyszła dana wartość.
- Nie wystawiaj samego Subjecta do zewnętrznego kodu. Jeśli każdy może wołać `next()`, tracisz kontrolę nad logiką przepływu.
- Nie zakładaj, że Subject pamięta wcześniejsze wartości. Jeśli subskrybent dołącza później, zwykły Subject nic mu nie odtworzy.
- Nie wybieraj BehaviorSubject tylko dlatego, że „coś trzeba”. Wymuszona wartość startowa czasem psuje model danych.
- Nie używaj Subjecta tam, gdzie wystarczy `share()`. Jeśli potrzebujesz tylko jednego współdzielonego źródła, ręczne emitowanie zwykle tylko komplikuje kod.
Dobry przykład prostszego podejścia to współdzielenie źródła zdarzeń bez ręcznego pośrednika:
import { fromEvent, map, share } from 'rxjs';
const clicks$ = fromEvent(document, 'click').pipe(
map(event => event.target),
share()
);Tutaj nie tworzę dodatkowego kanału tylko po to, żeby połączyć kilka subskrypcji. `share()` robi dokładnie tyle, ile trzeba: współdzieli jedno źródło i nie dokłada własnego imperatywnego API. Gdy potrzebuję zachować ostatnią wartość dla późniejszych subskrybentów, wtedy dopiero sięgam po `shareReplay()` albo ReplaySubject.
To prowadzi do ostatniej, praktycznej wskazówki: Subject jest narzędziem, nie architekturą. Jeśli zaczyna przejmować zbyt wiele ról, zwykle znaczy to, że kod można uprościć.
Jak sensownie korzystać z Subjecta w prawdziwym projekcie
Jeśli miałbym zamknąć ten temat w jednej zasadzie, powiedziałbym tak: Subject używam do świadomego emitowania zdarzeń, a nie do ukrywania logiki przepływu. Gdy potrzebuję wspólnego kanału dla wielu odbiorców, Subject sprawdza się bardzo dobrze. Gdy potrzebuję tylko współdzielenia źródła, częściej wygrywa `share()`. Gdy potrzebuję stanu, lepiej pasuje BehaviorSubject. Gdy potrzebuję historii, ReplaySubject. Gdy liczy się wyłącznie wynik końcowy, AsyncSubject.
To podejście trzyma kod blisko problemu, który naprawdę rozwiązuję, zamiast dokładać kolejną warstwę abstrakcji „na wszelki wypadek”. I właśnie dlatego Subject w RxJS jest tak użyteczny: jest prosty, ale tylko wtedy, gdy używa się go świadomie. Jeśli trzymasz się tej zasady, dostajesz czytelny multicast, a nie chaotyczny worek zdarzeń.