RxJS – 7 dobrych praktyk
Angular to obecnie jedno z najpopularniejszych narzędzi pozwalających na tworzenie aplikacji internetowych. Jedną z kluczowych składowych tej platformy jest biblioteka RxJS umożliwiająca pisanie reaktywnego kodu (ang. push-based approach). Programista Angular bardzo często ma do czynienia ze strumieniami danych, które obecne są w niemal każdym module tego frameworka.
W początkowym etapie kariery programiści mogą ograniczać się jedynie do subskrypcji do obecnych strumieni, jednak w pewnym momencie pojawia się konieczność poznania biblioteki RxJS w znacznie większym stopniu. W moim artykule przedstawię wybrane 7 dobrych praktyk i porad, które, mam nadzieję, poszerzą Twoją wiedzę na temat tej biblioteki.
Operator first
W pewnych sytuacjach jesteśmy zainteresowani tylko pojedynczą wartością wyemitowaną przez strumień danych. W takim przypadku możemy skorzystać z operatora first
lub take(1)
. Warto jednak pamiętać, że operator first
pozwala na przekazanie jako argument funkcji testującej, która zostanie wywołana z wartością pojawiającą się w strumieniu danych. W ten sposób możemy ograniczyć czas trwania subskrypcji do źródła danych do momentu emisji pierwszej wartości spełniającej funkcję testującą:
W powyższym przykładzie, subskrypcja do źródłowego strumienia (emitującego kolejne liczby naturalne w odstępie 1s) zostanie zakończona, gdy wyemitowana zostanie pierwsza wartość, dla której przekazana do operatora first
funkcja testująca zwróci wartość prawdziwą (ang. truthy value
), czyli w tym przypadku liczba 6.
Operator filter
Operator ten działa analogicznie jak metoda o tej samej nazwie obecna w prototypie tablic w JavaScript. Pozwala ona na filtrowanie wartości trafiających do wynikowego strumienia. Pisząc kod w Angularze, wykorzystujemy TypeScript, będący nadzbiorem języka JavaScript, który m.in pozwala na słabe statyczne typowanie. Jeżeli źródłowy strumień danych emituje obiekty będące instancją jednej z kilku klas, to możemy spodziewać się, że jeżeli odfiltrujemy wartości po ich typie, to w wynikowym strumieniu będziemy mieli dostęp do interfejsu tej konkretnej klasy. Nic bardziej mylnego:
W celu zawężenia typu na podstawie funkcji testującej przekazanej do operatora filter
, musimy zadeklarować typ zwracany z tej funkcji jako tzw. type predicate
:
Operator distinctUntilChanged
Jeżeli nie chcemy, aby w wynikowym strumieniu pojawiły się kolejno dwie takie same wartości, to możemy skorzystać z operatora distinctUntilChanged
. Problem pojawia się w momencie, gdy chcemy porównywać obiekty, ponieważ w JavaScript porównujemy referencje do obiektów. W rezultacie, obiekty mające takie same pola i ich wartości będą uznane są różne od siebie. Z pomocą przychodzi tutaj wersja operatora distinctUntilChanged
, do której przekazujemy funkcję testującą, na podstawie której stwierdzamy czy obiekty są sobie równe:
W powyższym przykładzie, strumień incorrectResults$
wyemituje obiekt o id=1
dwukrotnie, podczas gdy wersja z funkcją testującą da pożądany efekt.
Zjawisko wyścigu
Jeżeli pracujesz nad aplikacją, w której na podstawie interakcji użytkownika pobierasz dane odpowiadające aktualnym wartościom filtrów lub innych kontrolek, to zdecydowanie musisz zwrócić uwagę na zjawisko wyścigu. W skrócie, wykonujemy zapytanie http
w chwili t=0
oraz następne w dla t=5s
. Nie mamy gwarancji, że odpowiedź z serwera dla późniejszego zapytania pojawi się po odpowiedzi na wcześniejsze zapytanie. W rezultacie, może dojść do sytuacji, gdy będziemy wyświetlać nieaktualne dane dla wcześniejszych preferencji użytkownika. Biblioteka RxJS dostarcza operator switchMap
, który pozwala dzięki jednej linijce kodu poradzić sobie ze zjawiskiem wyścigu (ang. race condition):
W związku z tym, że w powyższym przykładzie wartości reprezentujące id
użytkownika emitowane są jedna po drugiej, zapytanie dla id=1
zostanie anulowane, dzięki czemu w wynikowym strumieniu pojawi się odpowiedź z serwera dla aktualnego id
użytkownika.
Niecierpliwy użytkownik
Zdarzają się sytuacje, gdy zniecierpliwiony użytkownik kilkukrotnie klika w przycisk interfejsu, np. w celu zalogowania się lub odświeżenia danych. W takich przypadkach wykonywanie ponownego zapytania w odpowiedzi na kolejne kliknięcia użytkownika nie ma najmniejszego sensu, co gorsza, zwiększa to czas potrzebny do otrzymania odpowiedzi z serwera. W takiej sytuacji możemy ponownie skorzystać z operatora, który dostarcza biblioteka RxJS, mianowicie exhaustMap
:
Operator exhaustMap
ignoruje wartości emitowane ze źródłowego strumienia, podczas gdy wewnętrzny strumień zwracany z tego operatora nie został zakończony - w powyższym przykładzie zakończenie wewnętrznego strumienia to moment, gdy otrzymamy odpowiedź z serwera. Reasumując, jeżeli zapytanie do serwera jest w trakcie (status pending
), to emisja wartości ze źródłowego strumienia nie będzie skutkowała wykonaniem kolejnego zapytania http
.
Zarządzanie subskrypcjami
Jeżeli posiadamy w naszej aplikacji miejsca, w których subskrybujemy się do źródeł danych, to należy pamiętać o anulowaniu takiej subskrypcji, tak aby nie tworzyć wycieków pamięci (ang. memory leaks). Zarządzanie subskrypcjami staje się problematyczne, gdy rośnie ich liczba:
Powyższe rozwiązanie zdecydowanie nie jest skalowalne, a liczba linijek kodu sugeruje, że musi istnieć lepsze rozwiązanie. Instancja klasy Subscription
posiada metodę add
, pozwalającą na dodanie do istniejącej subskrypcji kolejnych, które zostaną anulowane w momencie, gdy na początkowej instancji wywołamy metodę unsubscribe
:
Dodatkowo nie ma już konieczności sprawdzania, czy poszczególne subskrypcje są zdefiniowane przed wywołaniem na nich metody unsubscribe
.
Multicasting
W RxJS wyróżniamy tzw. hot & cold observables. Strumienie danych reprezentujące zapytanie http
to strumienie typu cold
, w związku z tym producent wartości jest tworzony w momencie subskrypcji do strumienia. W rezultacie, jeżeli taki strumień posłuży za źródłowy dla kilku wynikowych strumieni danych, to niepotrzebnie wykonamy kilka takich samych zapytań http
:
W powyższym przykładzie strumień reprezentujący zapytanie http
służy jako źródłowy strumień dla dwóch wynikowych, którymi emitowane są kolejno imię i email użytkownika. Jeżeli spojrzymy w konsolę, to zobaczymy, że wykonujemy dwa zapytania http
po ten sam zasób. Nie jest to pożądane, dlatego warto wykorzystać operator shareReplay
, który pozwala na tzw. multicasting
, czyli zamianą strumienia cold
na hot
:
Dzięki użyciu tego operatora zachowaliśmy wcześniejsze zachowanie i uniknęliśmy wykonywania niepotrzebnych zapytań do serwera po ten sam zasób.
Podsumowanie
Biblioteka RxJS to potężne narzędzie, które na początku może przytłaczać złożonością i liczbą dostarczanych operatorów. Z mojego doświadczenia wynika, że warto poświęcić czas na poznanie tej biblioteki, ponieważ pracując jako programista Angular mamy do czynienia z reaktywnym kodem niemalże w każdym miejscu aplikacji. Wykorzystanie biblioteki RxJS bardzo często pozwala w szybki i łatwy sposób rozwiązać złożone problemy, z którymi coraz częściej musi mierzyć się programista.