Wróć do strony głównej
NgRx

NgRx praktycznie – garść wskazówek

Uczysz się zarządzać stanem za pomocą NgRx? jeśli tak, to ten wpis jest skierowany do Ciebie. Nie będę obajśniał elementów NgRx, które pojawiają się w każdym tutorialu, czyli czym jest Redux i jaki jest flow, ani podstaw biblioteki NgRx. Zachęcam do poczytania dokumentacji: www.ngrx.io.

NgRx jest poteżnym narzędziem, ale niestety rzuca wiele kłód pod nogi programisty/stki. Zwłascza, jeśli nasze umiejętności w RxJS nie są na najwyższym poziomie. Podzielę się z Tobą wiedzą w kwestiach, które mogą być dla Ciebie problematyczne w pierwszych tygodniach pracy.

1. Kształtowanie Store

Pierwsza zagadka – jak trzymać dane dla dużych kolekcji? czy tablica? czy obiekt z kluczami?

ŹLE:

  • w przypadku kolekcji, aby wyciągnąć jeden samochód z listy po ID, będziemy musieli przeszukać całą kolekcję, np. za pomocą metody Array.find (złożoność obliczeniowa O(N)):

DOBRZE:

  • Lepszym rozwiązaniem jest trzymać encje w obiekcie, gdzie kluczami są odpowiadające ID. Dzięki temu wydajniej wyszukujemy poszczególne elementy (złożoność obliczeniowa O(1)):

Minusem powyższego rozwiązania jest utrata informacji o kolejności obiektów, co jest zapewnione w przypadku listy. Aby rozwiązać ten problem, możemy w Store trzymać tablicę trzymają odpowiednią kolejność ID:

Inną opcją jest użycie biblioteki @ngrx/entity, która zadba o kolejność i dostarczy nam pakiet selektorów.

2. Feature modules

Kolejnym poważnym błędem w przypadku aplikacji złożonej z modułów, jest trzymanie wszystkiego w jednym module. Załóżmy, że posiadamy następujące moduły funkcjonalne: Products, Offers, Orders, Admin. W tym przypadku, każdy moduł powinien mieć swój dedykowany Store, a sam NgRx zadba o to, aby wszystko złożyć do kupy. Działa to również bardzo dobrze z Lazy Modules, po załadowaniu modułu, dany FeatureStore doklei się do globalnego Store.

Store module diagram

Wykorzystujemy do tego metodę forFeature:

 

3. Feature Module, mapa reducerów czy jeden reducer?

Warto wiedzieć, że do FeatureStore możemy przekazać jeden reducerFn, zamiast mapy reducerów. Zredukuje nam to liczbę zagnieżdzeń dla prostych przypadków.

4. Akcja – payload zawsze jako obiekt.

Dobrą praktyką jest trzymać payload zawsze pod obiektem, nawet jak jest tylko jedna wartość. Poprawia to znacznie czytelność, zwłaszcza w reducerach i efektach, gdzie odnosimy się do payloadu.

ŹLE:

DOBRZE:

5. combineLatest – wyciągamy wiele wartości ze Store

W NgRx bardzo często dochodzi do sytuacji, kiedy chcemy wyciągnąć ze Store wiele wartości jednocześnie. Z pomocą przychodzi nam Observable Creator – CombineLatest:

Pamiętaj, aby zrobić unsubscribe na subskrypcji, np. w hooku ngOnDestroy oraz, że callback w subscribe uruchomi się za każdym razem, gdy OBOJĘTNIE który ze strumieni wyemituje (getUser lub cars), ale pod warunkiem, że każdy już coś wyemitował.

6. withLatestFrom – wyciągamy wiele wartości ze Store, ale nie nasłuchujemy na nie

Różnica między CombineLatest a withLatestFrom jest taka, że w przypadku CombineLatest, utworzony strumień będzie emitować za każdym razem, gdy którykolwiek z przekazanych strumieni wyemituje, natomiast w przypadku withLatestFrom, chcemy nasłuchiwać tylko na strumień źródłowy – w poniższym przypadku getUser$:

WithLatestFrom często wykorzystuje się w efektach:

WithLatestFrom jest również pomocny, jeśli chcemy wyciągnąć kawałek Store z innego modułu, wystarczy do niego przekazać główny selektor danego modułu.

7. Dispatch: false w efektach

Standardowo, efekt nasłuchuje na daną akcję i zwraca inną:

 

Jeśli efekt nie będzie zwracał żadnej akcji, to zwróci tą, na którą nasłuchuje w OfType. Można popaść przez to w problemy! Gdy nie chcemy w efekcie zwracać akcji – bo np. tylko chcemy zawołać router.navigate, to przekazujemy do dekoratora @Effect obiekt z właściwością dispatch:

8. Wiele akcji w ofType

Widziałem już przypadki duplikacji efektów. Dobrze wiedzieć, że do ofType możemy przekazać wiele akcji na raz.

ŹLE:

DOBRZE:

9. Efekt z dowolnego strumienia

Efekt nie musi polegać tylko na akcjach przepływających przez Store. Możemy nasłuchiwać na dowolny Observable:

W powyższym przypadku, emituje akcję ChangeOnlineStatus przy każdej zmianie statusu online przeglądarki.

10. Zwrócenie wielu akcji na raz

Jeśli chcesz zwrócić wiele akcji na raz, np. w efekcie, to wykorzystaj concatMap.

 

11. Nie żałuj akcji

Nie rób akcji z flagami. Lepsze są dwie akcje bez payloadu w takich przypadkach. Krócej, nie znaczy lepiej!
ŹLE:

DOBRZE:

12. Operator First()

Od Store musimy się odsubskrybować, z wyjątkiem użycia AsyncPipe w templatce. Bardzo często wystarczy użyć operatora first, aby nie bawić się w unsubscribe.
TAK SOBIE:

LEPIEJ:

First() skompletuje strumień po wyemitowaniu pierwszej wartości. Tym niemniej uważnie! bo już dalej nie będziesz nasłuchiwał na emitowane wartości tego strumienia.

13. Parametryzowane selektory

Zdarza się, że chcemy wyciągnąć coś ze Store na podstawie parametru. Możemy zrobić to starą szkołą (NgRx < v7.0), czyli napisać funkcję, która zwraca selektor. Niestety, w tym przypadku nie działa memoizacja i dla tego samego parametru, selektor zostanie przeliczony na nowo.

Od wersji NgRx > v7.0.0, mamy dostęp do selektor props:

Ogólne wskazówki:

– korzystaj z NgRx Schematics, aby szybko setupować Store
– zastanów się zawsze 3 razy, czy coś na pewno powinno być w Store, być może wystarczy lokalny stan komponentu, np. dla stanu formularza
– zbyt wiele komponentów świadomych Store -> nie dopuść do tego, rozważ zawsze czy na pewno chcesz wstrzyknąć Store do danego komponentu (może rodzic ma już Store i może poprzez @Input przekazać wartość do dziecka?)
– zawsze typuj cały State i payload akcji, nie pozwól sobie na żadne „any”, szybko stracisz kontrolę nad tym co wpada i wychodzi ze Store
– zawsze rób unsubscribe na selektorach, chyba, że użyłeś np. operatora first() lub nasłuchujesz poprzez AsyncPipe.
– reducery trzymaj możliwie proste, ich logika powinna być uboga
– wykorzystuj selektory z logiką, aby łączyć dane ze Store. Nie duplikuj parę razy CombineLatest w wielu miejscach, lepiej stwórz selektor pod to korzystający z dwóch innych selektorów
– staraj się trzymać płaski State, im mniej zagnieżdzeń tym lepiej
– nie dubluj tych samych wartości w Store. Zamiast trzymać w Store zaznaczone obiekty w tabeli, trzymaj wyłącznie ich IDs. Nie będziesz musiał się martwić o akutalizację danych we wszystkich miejscach w przypadku zmian.
– zawsze testuj główne składowe -> selektory, reducery, efekty oraz Store w komponentach, czy. np dispatchuje akcje na click.
– użyj Redux DevTools do debugowania i time travelingu
– użyj biblioteki ngrx-store-freeze, aby mieć pewność, że nie mutujesz nigdzie stanu

Podsumowanie

NgRx jest potężnym narzędziem, ale jednocześnie dość skomplikowanym, z dużym boilerplate i narzutem wiedzy w postaci RxJS. Jest dużo miejsc, gdzie można coś spieprzyć. Moim zdaniem, nadaje się dobrze do dużych, złożonych aplikacji, ale do czegoś mniejszego, protszego, wybrałbym MobX lub NGXS.

Być może masz pytania z NgRx? Jeśli tak, to pytaj śmiało w komentarzach 🙂

 

O autorze

Tomasz Nastały

JavaScript Developer, entuzjasta frameworka Angular oraz TypeScripta. Na co dzień lubię dzielić się wiedzą poprzez prowadzenie zajęć w jednym z trójmiejskich bootcampów i nagrywaniem kursów z Angulara.

Zapisz się do naszego newslettera. Bądź na bieżąco z najnowszymi trendami, poradami, meetupami i stań się częścią społeczności Angulara w Polsce. Rynek pracy docenia członków społeczności.

9 komentarzy

  1. Konrad Klimczak

    Świetny artykuł, który w szczególności pozwoli odnaleźć się „nowym” w ngrxie. 🙂 Jednak nie moge się zgodzić z punktem 10 z dwóch powodów.
    1. W całym ekosystemie ngrx powinniśmy traktować akcję jako unikalne zdarzenie i nie emitować wielu akcji w tym samym czasie z jednego miejsca. Pozwala na mniej bolesne debugowanie poszczególnych stanów aplikacji oraz zachowanie lepszej higieny naszego store’a. Emitowanie kilku akcji niestety powoduje też produkcję kolejnych stanów aplikacji. Dodatkowo może dochodzić do zachowań typu race condition, na które sam jak zaczynałem też się kiedyś naciąłem.
    2. Używanie efektu wyłącznie po to by wysłać kolejne akcje jest złym wykorzystaniem efektów, które przeznaczone są przede wszystkim do pracy z side effectami, na które nie mamy wpływu, tzn. nie mamy pojęcia kiedy pojawi się się z niego odpowiedź. Tą kwestię jak i parę innych odnośnie używania efektów świetnie porusza ten artykuł: https://medium.com/@m3po22/stop-using-ngrx-effects-for-that-a6ccfe186399.

    Zapraszam do dyskusji ala „Change My Mind”, bo dzielenie się dobrą wiedzą też jest również dobrą praktyką. 🙂
    Pozdrawiam 😀

  2. Wojciech Trawiński

    A ja pozwolę się nie zgodzić z Konrad Klimczak.

    Jeżeli swoją opinię budujesz na podstawie https://www.youtube.com/watch?v=JmnsEvoy-gY to nie polecam.

    Zauważ, że komponenty powinny emitować akcję jako sygnał, że wydarzył się jakiś event (event action to jeden z 3 rodzajów akcji, pozostałe to command i document). Komponentu nie interesuje czy w związku z tym, że zainicjalizował się ma nastąpić pobranie danych czy też lot w kosmos. Jest to luźne powiązanie i dzięki temu zawsze możesz stworzyć dodatkowy efekt który nasłuchuje na tą akcję bądź w istniejącym efekcie zwrócić dodatkową akcję.

    W poprzedniej pracy stworzyliśmy dwie duże aplikacje bazujące na tym flow i nowe osoby w projekcie bardzo chwaliły sobie czytelność kodu.

    Pozdrawiam

  3. Konrad Klimczak

    Zgodzę się z Tobą, że po komponent ma wyłącznie poinformować o utworzeniu siebie i co za tym idzie wywołać efekt, ale bardziej mi chodzi o niepotrzebne komplikowanie łańcucha wywołań do pobrania danych, bo w twoim przypadku mamy:
    LoginSuccess => LoadUsers => LoadUsers{Success|Failure}
    a dlaczego nie uprościć do postaci:
    LoginSuccess => LoadUsers{Success|Failure}
    Nie widzę sensu tworzenia akcji tylko po to by została następnie przekonwertowana przez kolejny efekt na akcję wynikową. U mnie w projekcie osoby narzekały, że muszą robić dodatkową akcję (tj. action producer), która nic nie wnosi do stanu aplikacji, bo jest od razu łapana przez kolejny efekt. Po zmianie podejścia mamy zdecydowanie mniej kodu, który nie traci na czytelności, a zyskujemy na mniejszej ilości akcji, a co za tym idzie, dużo łatwiejsze odnalezienie się w DevToolsach.

  4. Tomasz

    Też się nie zgadzam z Konradem. Side effects często mi służyły do rozdzielania/agregowania akcji. Komponent wysyła tylko event/command którym informuje o zdarzeniu/czego wymaga. U mnie w projekcie własnie side effect potrafił wyrzucać akcje żeby rozbijać to na kolejne, ponieważ komponent nie powinien sterować.

    https://blog.nrwl.io/ngrx-patterns-and-techniques-f46126e2b1e5
    Tutaj bardzo fajnie są opisane design patterny z użyciem side Effects. Jest tam wąłśnie mowa agregowaniu, rozdzielaniu i decydowaniu na bazie akcji oraz contentu

    przykłąd 10 idealnie pokazuje splittera. Pobierz dodatkowe rzeczy, które są reakcją na akcje

  5. Krzysztof Podlaski

    A jak proponujecie np zamkniecie modala club inna akcje w komponecie po zakonczeniu effectu. Przeslanie referencji w akcji I wywolnie w tap, nasluchiwanie na effect w komponecie (nie jest to zbytnie zwiazanie komponentu w ngrx framework). Dodatkowo interesuje mine wasza opinie zeby ngrxowe selecty i dispatch akcji umiescic w dodatkowej warstwie abstrakcji, someStateService,. I poprzez DI inicjalizowac observable properties w kontenerze a później przez async pipe do komponentu. Mam nadzieję że napisałem to w miarę jasno

  6. Tomasz

    Jeżeli chodzi o obsługę modali to zależy od podejścia.
    Fajnie wygląda to w materialu, bo dialog ma referencję do siebie. Na close, zamyka sam siebie oraz leci observable z resultem, więc w side effect otwierasz modala i obsługujesz observable po zamknięciu:

    @Effect()
    openDialog = this.actions.pipe(
    ofType(LoginActionTypes.OpenLoginDialog),
    exhaustMap(_ => {
    let dialogRef = this.dialog.open(LoginDialog);
    return dialogRef.afterClosed();
    }),
    map((result: any) => {
    if (result === undefined) {
    return new CloseDialog();
    }
    return new LoginDialogSuccess(result);
    }),
    );

    W PrimeNg mieli inne podejście (nie wiem jak teraz). Dialogi były renderowane w kontenerze, i wtedy był ngIf na wartość ze store, czy dany dialog powinien byc renderowany. Side effect nic nie robił, akcje na open, close leciały z konenera renderującego dany dialog i nasłuchującego stan

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *