Na naszym blogu pojawił się już kiedyś wpis o wskazówkach do NgRx autorstwa Tomasza Nastałego, który mocno zachęcam przeczytać. Po długiej przerwie nadeszła pora na następną porcję tips & tricks – zapraszam!
1. withLatestFrom -> concatLatestFrom
Z wpisu, który zalinkowałem we wstępie wiesz już, że gdy potrzebujesz skorzystać w efekcie z wartości znajdującej się w state, dobrym rozwiązaniem jest użycie operatora withLatestFrom. Niewiele osób jednak wie, że użycie tego operatora bez skorzystania z flattening operatora (switchMap, concatMap, mergeMap, exhaustMap) ma skutek uboczny. Operator withLatestFrom subskrybuje się, nawet jeśli główny observable w efekcie nie wyemituje wartości. Jeżeli potrzebujesz stworzyć efekt bez używania flattening operatora, zamiast withLatestFrom użyj concatLatestFrom. Dla przykładu:
Tutaj nastąpi subskrypcja selektora fromBooks.getCollectionBookIds niezależnie od tego czy akcja AddBookSuccess zostanie zdispatchowana:
1 2 3 4 5 6 7 8 9 10 11 |
@Injectable() export class CollectionEffects { addBookToCollectionSuccess$ = createEffect( () => this.actions$.pipe( ofType(CollectionApiActions.addBookSuccess), withLatestFrom( this.store.select(fromBooks.getCollectionBookIds)), tap(([action, bookCollection]) => { ... |
Tutaj natomiast selektor fromBooks.getCollectionBookIds zasubskrybuje się tylko wtedy, gdy akcja AddBookSuccess zostanie zdispatchowana:
1 2 3 4 5 6 7 8 9 |
@Injectable() export class CollectionEffects { addBookToCollectionSuccess$ = createEffect( () => this.actions$.pipe( ofType(CollectionApiActions.addBookSuccess), concatLatestFrom(action => this.store.select(fromBooks.getCollectionBookIds)), tap(([action, bookCollection]) => { ... |
2. Nx DataPersistence kluczem do dobrych efektów
Nx DataPersistence dostarcza zestaw pomocniczych funkcji, które umożliwiają deweloperowi zarządzanie stanem w Angularze z uwzględnieniem strategii synchronizacji i obsługi błędu. Są to:
- Optimistic update – w pierwszej kolejności aktualizuje dane po stronie klienta, dopiero później pozwala je synchronizować z danymi uzyskanymi z backendu. Na wypadek niepowodzenia aktualizacji danych po stronie serwera dostarcza nam handler undoAction, który pozwala na wycofanie wprowadzonych zmian po stronie klienta.
- Pessimistic update – odwrotność powyższego. Najpierw aktualizuje dane po stronie api, a po pomyślnej operacji aktualizuje dane po stronie klienta. W razie niepowodzenia operacji po stronie serwera, nie ma potrzeby cofania wywołanych zmian, jednak na czas oczekiwania na odpowiedź serwera warto zadbać o poinformowanie użytkownika o trwającej operacji (np. przy użyciu spinnera).
- Fetch – pobieranie danych. Ważnym elementem jest możliwość przekazania id pobieranego obiektu do efektu, dzięki czemu w razie wywoływania tej samej akcji dla różnych obiektów, zapytania wykonają się równolegle.
- Navigate – pozwala na sprawdzenie czy aktywowana ścieżka zawiera przekazany w parametrze komponent i jeśli tak jest, wykonanie zadanego polecenia.
Nx DataPersistance bardzo ułatwia zarządzanie stanem z NgRx, przez co zachęcam do przeczytania dokumentacji i wdrożenia go do swojego projektu.
3. Nawigacja z użyciem NgRx
W miarę rozrastania się naszej aplikacji zarządzanie nawigacją może się skomplikować. Do ułatwienia debugowania routingu Twojej aplikacji użyj router-store’a. Od teraz store będzie dispatchował szereg akcji przy każdej uruchomionej nawigacji, co w połączeniu z Redux Devtoolsami pozwala na prześledzenie krok po kroku tego się dzieje w nawigacji.
Dodatkowo, jeżeli w projekcie korzystasz z @ngrx/entity, możesz użyć selektorów przygotowanych pod korzystanie z router-store. Dają one m.in. możliwość wybierania danych ze store w oparciu o ścieżkę url, bez konieczności pobierania informacji o route w komponencie.
4. Między stanem, a komponentem – fasada
Aby ułatwić organizację komponentu korzystającego ze store i poprawić czytelność kodu możesz użyć fasady. Fasada jest odpowiedzialna za utworzenie dodatkowej warstwy między komponentem, a stanem aplikacji. Możesz ją rozumieć jak API, które jest udostępnione dla komponentów przez store. Nie będę rozpisywać się o tym jak utworzyć fasadę, gdyż jest już na ten temat świetny artykuł od Thomasa Burlesona. Od siebie dodam, że po implementacji pierwszej fasady nie chciałem i nie wróciłem już nigdy do bezpośredniego używania store w komponentach.
5. Runtime checks
Jeżeli chcesz mieć pewność, że Twoje funkcjonalności oparte o NgRx są zgodne z jego kluczowymi konceptami, skorzystaj z runtime checks – opcji do konfiguracji store’a. Runtime checks umożliwiają sprawdzanie działania naszego kodu i w razie potrzeby informowanie o błędzie w konsoli. Jest to bardzo pomocna rzecz w trakcie developmentu. Jeżeli chcesz poznać wszystkie opcje konfiguracyjne, odsyłam Cię do świetnie napisanej dokumentacji.
6. Wyłuskiwanie payloadu w efekcie
Jeżeli do akcji przekazujesz obiekt i chcesz wydobyć jego wartość w efekcie, możesz użyć operatora pluck.
Gdybyśmy chcieli użyć takiej akcji:
1 2 3 4 |
export const removeBook = createAction( '[Book Collection] Remove Book', props<{ bookId }>() ); |
to efekt prawdopodobnie wyglądałby tak:
1 2 3 4 5 6 7 8 |
removeBook$ = createEffect(() => this.actions$.pipe( ofType(removeBook), switchMap((action) => this.booksService.removeBook(action.bookId).pipe( map(() => removeBookSuccess({bookId: action.bookId})) ) ) )); |
Z użyciem operatora pluck pozbywamy się konieczności wielokrotnego odnoszenia się do wartości bookId z action:
1 2 3 4 5 6 7 8 9 |
removeBook$ = createEffect(() => this.actions$.pipe( ofType(removeBook), pluck('bookId'), switchMap((bookId) => this.booksService.removeBook(bookId).pipe( map(() => removeBookSuccess({bookId})) ) ) )); |
Na dzisiaj koniec!
Dajcie znać w komentarzach czy dzięki temu wpisowi dowiedzieliście się czegoś nowego i czy chcielibyście więcej tego rodzaju contentu na blogu!
Świetny artykuł gorąco pozdrawiam Panie Adamie. Nie dość, że przystojny mężczyzna, to jeszcze świetne blogi.
Część 🙂 chciałbym się dopytać odnośnie tego plucka, z racji że mamy payload w obiekcie akcji to zamiast konwertować streama kolejnym operatorem nie lepiej będzie zastosować po prostu destrukturyzację? Np. switchMap ({ bookId }) => …
Cześć, dzięki za komentarz! Masz rację, oczywiście możemy użyć destrukturyzacji. W przypadku podanym w artykule byłoby to krótsze rozwiązanie 🙂
Ja w swoich projektach z reguły tworzę dedykowane payloady na akcje, np. GetBookPayload { bookId: string }, przez co przy użyciu pluck(’payload’) ewentualne rozszerzenie payloadu o kolejnego propsa nie wymaga ingerencji w sam effect, o ile ten effect ma na celu tylko zawołanie do data-service i przekazanie zwrotki z api do akcji na success.
W tym wpisie chciałem wspomnieć o pluck’u, ponieważ z mojego doświadczenia jest to rzadko spotykany i często nieznany przez developerów operator.
Pan Adam fachura!!!!
Witam, świetny art.
Mam pytanie ponieważ nigdzie nie mogę znaleźć informacji na ten temat, a śledzę Waszego bloga i widzę, że obracacie się nieźle w temacie. Korzystam z Waszych rad 😉
W module mam podzielone tematycznie reducery, a struktura wygląda tak, że posiadam główny state (powiedzmy MainState), na którym używam ActionReducerMap, aby zagnieździć tematyczne states. Jeśli z poziomu komponentu odwołuje się do selektora w danym tematycznym state to czy jest różnica gdy do constructora przekażę MainState czy ten tematyczny state (czyli zagnieżdżony)? Czy mogę posługiwać się MainState i nie ma to żadnego znaczenia?
Pozdrawiam serdecznie!
Cześć, dzięki za pozytywny feedback 🙂
Co do Twojego pytania, to z punktu Angulara nie ma różnicy jak zatypujesz store w konstruktorze, jednak w celach większej czytelności kodu zalecam typowanie konkretnego, tematycznego state’u (o ile oczywiście wykorzystujesz selectory tylko z niego, a nie korzystasz dodatkowo z pozostałych, zagnieżdżonych state’ów), żeby z góry było jasne jaka część stanu jest wykorzystywana w komponencie.
Tak też czułem. Oprócz tego widzę taką różnicę, że mając 2 selektory o tych samych nazwach, korzystając z głównego state w konstruktorze, można się pomylić w imporcie.
Wielkie dzięki !
Pozdrawiam
Pluck jest spoko, ale trzeba pamiętać, że jest również bugo-genny, jeżeli przyjdzie refactorować obiekt przesyłany przez akcje, to automatyczny refactor większości IDE nie ogarnie plucka, i nie zmieni tam wartości 😛
Pingback: NgRx – tips & tricks - Angular.love