Fundamentalne elementy NgRx: Store, Actions, Reducers, Selectors, Effects
Sklep jest kluczowym elementem w całym procesie zarządzania stanem. Przechowuje on stan i ułatwia interakcję pomiędzy komponentami a stanem. Możesz uzyskać referencję do sklepu poprzez Angular dependency injection, jak pokazano poniżej.
constructor(private store: Store<AppState>) {}
{ "type": "Login Action", "payload": { userProfile: user }}
NgRx w wersji 8 udostępnia funkcję użytkową o nazwie createAction
do definiowania twórców akcji (nie akcji, lecz twórców akcji). Poniżej znajduje się przykładowy kod do tego celu.
Możesz następnie użyć kreatora akcji login
(który jest funkcją) do budowania akcji i wysyłania ich do sklepu, jak pokazano poniżej. user
jest obiektem payload
, który przekazujemy do akcji.
this.store.dispatch(login({user}));
Reducery
Reducery są odpowiedzialne za modyfikację stanu i zwrócenie nowego obiektu stanu z modyfikacjami. Reduktory przyjmują dwa parametry, aktualny stan oraz akcję. W oparciu o otrzymany typ akcji, reduktory wykonają pewne modyfikacje bieżącego stanu i wyprodukują nowy stan. Koncepcja ta została przedstawiona na poniższym diagramie.
Podobnie do akcji, NgRx udostępnia funkcję użytkową o nazwie createReducer
do tworzenia reduktorów. Typowe wywołanie funkcji createReducer
wyglądałoby następująco.
Jak widać, przyjmuje ona stan początkowy (stan podczas uruchamiania aplikacji) oraz funkcje zmiany stanu typu jeden do wielu, które definiują sposób reagowania na różne akcje. Każda z tych funkcji zmiany stanu otrzymuje bieżący stan i akcję jako parametry, i zwraca nowy stan.
Efekty
Efekty pozwalają na wykonywanie efektów ubocznych, gdy akcja jest wysyłana do sklepu. Spróbujmy to zrozumieć na przykładzie. Kiedy użytkownik pomyślnie zaloguje się do aplikacji, akcja z type
Login Action
zostanie wysłana do sklepu z informacjami o użytkowniku w payload
. Funkcja reduktora będzie nasłuchiwała tej akcji i modyfikowała stan z informacjami o użytkowniku. Dodatkowo, jako efekt uboczny, chcesz również zapisać informacje o użytkowniku w lokalnym magazynie przeglądarki. Efekt może być użyty do wykonania tego dodatkowego zadania (efekt uboczny).
Istnieje wiele sposobów tworzenia efektów w NgRx. Poniżej znajduje się surowy i samowyjaśniający się sposób tworzenia efektów. Zwróć uwagę, że generalnie nie używasz tej metody do tworzenia efektów. Wziąłem to tylko jako przykład, aby wyjaśnić, co dzieje się za kurtyną.
-
actions$
obserwowalne będą emitować akcje otrzymane przez sklep. Wartości te będą przechodziły przez łańcuch operatorów. -
ofType
jest pierwszym użytym operatorem. Jest to specjalny operator dostarczany przez NgRx (Not RxJS) do filtrowania akcji na podstawie ich typu. W tym przypadku, tylko akcje typulogin
będą mogły przejść przez resztę łańcucha operatorów. -
tap
jest drugim operatorem używanym w łańcuchu do przechowywania informacji o użytkowniku w lokalnej pamięci masowej przeglądarki. Operatortap
jest zwykle używany do wykonywania efektów ubocznych w łańcuchu operatorów. - W końcu musimy ręcznie zasubskrybować obserwowalną
login$
.
Jednakże to podejście ma kilka poważnych wad.
- Musisz ręcznie zasubskrybować obserwowalną, co nie jest dobrą praktyką. W ten sposób zawsze będziesz musiał ręcznie się wypisać, co prowadzi do braku łatwości utrzymania.
- Jeśli w łańcuchu operatora wyskoczy błąd, obserwowalny popełni błąd i przestanie emitować kolejne wartości (akcje). W efekcie efekt uboczny nie zostanie wykonany. Dlatego trzeba mieć mechanizm do ręcznego tworzenia nowej instancji obserwowalnej i ponownej subskrypcji, jeśli wystąpi błąd.
Aby przezwyciężyć te problemy, NgRx dostarcza funkcję użytkową o nazwie createEffect
do tworzenia efektów. Typowe wywołanie funkcji createEffect
wyglądałoby następująco.
Metoda createEffect
przyjmuje funkcję, która zwraca obserwowalną i (opcjonalnie) obiekt konfiguracji jako parametry.
NgRx obsługuje subskrypcję obserwowalnej zwracanej przez funkcję pomocniczą, dlatego nie trzeba ręcznie subskrybować ani wypisywać się z niej. Ponadto, jeśli wystąpi jakikolwiek błąd w łańcuchu operatora, NgRx utworzy nową obserwowalną i ponownie zasubskrybuje, aby zapewnić, że efekt uboczny zawsze zostanie wykonany.
Jeśli dispatch
jest true
(wartość domyślna) w obiekcie konfiguracyjnym, metoda createEffect
zwraca Observable<Action>
. W przeciwnym przypadku zwraca Observable<Unknown>
. Jeśli właściwość dispatch
ma wartość true
, NgRx zasubskrybuje zwróconą obserwowalność type
Observable<Action>
i wyśle otrzymane akcje do sklepu.
Jeśli nie mapujesz otrzymanej akcji do innego typu akcji w łańcuchu operatora, będziesz musiał ustawić dispatch
na false
. W przeciwnym razie wykonanie spowoduje nieskończoną pętlę, ponieważ ta sama akcja będzie wysyłana i odbierana w strumieniu actions$
ponownie i ponownie. Na przykład, nie musisz ustawiać dispatch
na false
w poniższym kodzie, ponieważ mapujesz oryginalną akcję na inny typ akcji w łańcuchu operatora.
W powyższym scenariuszu,
- Efekt odbiera akcje
type
loadAllCourses
. - Wywoływane jest API, a kursy są ładowane jako efekt uboczny.
- Odpowiedź API na akcję o wartości
type
allCoursesLoaded
jest mapowana, a załadowane kursy są przekazywane jakopayload
do akcji. - I w końcu, utworzona
allCoursesLoaded
akcja jest wysyłana do sklepu. Jest to wykonywane przez NgRx pod maską. - Reduktor będzie nasłuchiwał przychodzącej
allCoursesLoaded
akcji i modyfikował stan za pomocą załadowanych kursów.
Selektory
Selektory są czystymi funkcjami używanymi do uzyskiwania plasterków stanu sklepu. Jak pokazano poniżej, możesz zapytać o stan nawet bez użycia selektorów. Ale to podejście znów ma kilka poważnych wad.
const isLoggedIn$ = this.store.pipe(map(state => !!state.user));
-
store
jest obserwowalną, którą można subskrybować. Ilekroć sklep otrzyma akcję,store
popchnie obiekt stanu do swoich subskrybentów. - Możesz użyć funkcji mapowania, aby uzyskać plasterki stanu i wykonać dowolne obliczenia, jeśli jest to wymagane. W powyższym przykładzie uzyskujemy plaster
user
w drzewie obiektów stanu i konwertujemy go na boolean, aby określić, czy użytkownik zalogował się, czy nie. - Możesz albo ręcznie zasubskrybować obserwowalne
isLoggedIn$
, albo użyć go w szablonie Angular z rurą async do odczytania emitowanych wartości.
Jednakże to podejście ma poważną wadę. Ogólnie rzecz biorąc, sklep otrzymuje akcje często z różnych części aplikacji. Zgodnie z powyższą implementacją, za każdym razem, gdy sklep otrzyma akcję, obiekt stanu zostanie wyemitowany przez sklep. I ten obiekt stanu ponownie przejdzie przez funkcję odwzorowania i zaktualizuje UI.
Jednakże, jeśli wynik funkcji odwzorowania nie zmienił się od ostatniego razu, nie ma potrzeby ponownego aktualizowania UI. Na przykład, jeśli wynik map(state => !!state.user)
nie zmienił się od ostatniego wykonania, nie musimy ponownie przesuwać wyniku do UI/Subskrybenta. Aby to osiągnąć, NgRx (nie RxJS) wprowadził specjalny operator o nazwie select
. Dzięki operatorowi select
, powyższy kod zmieni się w następujący sposób.
const isLoggedIn$ = this.store.pipe(select(state => !!state.user));
Operator select
zapobiegnie wypychaniu wartości do UI/subskrybentów, jeśli wynik funkcji mapującej nie zmienił się od ostatniego razu.
To podejście można jeszcze ulepszyć. Nawet jeśli operator select
nie popycha niezmienionych wartości do UI / subskrybentów, nadal musi wziąć obiekt stanu i wykonać obliczenia, aby uzyskać wynik za każdym razem.
Jak już wyjaśniono powyżej, state
będzie emitowane przez obserwowalne, gdy sklep otrzyma akcję z aplikacji. Akcja nie zawsze aktualizuje stan. Jeśli stan się nie zmienił, wynik obliczeń funkcji odwzorowującej również się nie zmieni. Dlatego nie musimy wykonywać obliczeń ponownie, jeśli emitowany obiekt state
nie zmienił się od ostatniego razu. W tym miejscu do gry wchodzą selektory.
Selektor jest czystą funkcją, która zachowuje pamięć poprzednich wykonań. Tak długo, jak dane wejściowe nie uległy zmianie, dane wyjściowe nie zostaną ponownie obliczone. Zamiast tego, wyjście zostanie zwrócone z pamięci. Proces ten nazywany jest memoizacją.
NgRx dostarcza funkcję użytkową o nazwie createSelector
do budowania selektorów z możliwością memoizacji. Poniżej przedstawiono przykład funkcji użytkowejcreateSelector
.
Funkcja createSelector
przyjmuje funkcje odwzorowania jeden do wielu, które dają różne wycinki stanu i funkcję projektora, która wykonuje obliczenia. Funkcja projektora nie będzie wywoływana, jeśli plasterki stanu nie zmieniły się od ostatniego wykonania. Aby użyć utworzonej funkcji selektora, należy przekazać ją jako argument do operatora select
.
this.isLoggedIn$ = this.store .pipe( select(isLoggedIn) );