Głębokie nurkowanie w zaletach i funkcjach NgRx

Opublikowany: 2022-03-11

Jeśli lider zespołu poinstruuje programistę, aby napisał dużo standardowego kodu zamiast pisać kilka metod rozwiązania określonego problemu, potrzebuje przekonujących argumentów. Inżynierowie oprogramowania rozwiązują problemy; wolą automatyzować rzeczy i unikać niepotrzebnego schematu.

Mimo że NgRx jest dostarczany z pewnym standardowym kodem, zapewnia również potężne narzędzia do rozwoju. Ten artykuł pokazuje, że poświęcenie nieco więcej czasu na pisanie kodu przyniesie korzyści, które sprawią, że będzie to warte wysiłku.

Większość programistów zaczęła używać zarządzania stanem, gdy Dan Abramov wydał bibliotekę Redux. Niektórzy zaczęli używać zarządzania państwem, ponieważ był to trend, a nie dlatego, że go brakowało. Deweloperzy korzystający ze standardowego projektu „Hello World” do zarządzania stanem mogą szybko znaleźć się w sytuacji, gdy piszą w kółko ten sam kod, zwiększając złożoność bez żadnych korzyści.

W końcu niektórzy zostali sfrustrowani i całkowicie porzucili zarządzanie państwem.

Mój początkowy problem z NgRx

Myślę, że ten schematyczny problem był głównym problemem w przypadku NgRx. Na początku nie byliśmy w stanie zobaczyć dużego obrazu, który się za tym kryje. NgRx to biblioteka, a nie paradygmat programowania czy sposób myślenia. Aby jednak w pełni zrozumieć funkcjonalność i użyteczność tej biblioteki, musimy nieco bardziej poszerzyć naszą wiedzę i skupić się na programowaniu funkcjonalnym. Wtedy możesz zacząć pisać standardowy kod i czuć się z tego powodu szczęśliwy. (Mam na myśli to.) Byłem kiedyś sceptykiem NgRx; teraz jestem wielbicielem NgRx.

Jakiś czas temu zacząłem korzystać z zarządzania państwem. Przeszedłem przez opisane powyżej schematy, więc postanowiłem przestać korzystać z biblioteki. Ponieważ kocham JavaScript, staram się zdobyć przynajmniej podstawową wiedzę na temat wszystkich popularnych obecnie używanych frameworków. Oto czego nauczyłem się podczas korzystania z Reacta.

React ma funkcję o nazwie Hooki. Podobnie jak Components w Angularze, hooki są prostymi funkcjami, które akceptują argumenty i zwracają wartości. Haczyk może mieć w sobie stan, który nazywa się efektem ubocznym. Na przykład prosty przycisk w Angularze można by przetłumaczyć na Reacta w ten sposób:

 @Component({ selector: 'simple-button', template: ` <button>Hello {{ name }}</button> `, }) export class SimpleButtonComponent { @Input() name!: string; } export default function SimpleButton(props: { name: string }) { return <button>{props.name} </button>; }

Jak widać, jest to prosta transformacja:

  • SimpleButtonComponent => SimpleButton
  • @Input() name => props.name
  • szablon => zwracana wartość

Ilustracja: Angular Component i React Hooks są dość podobne.
Angular Component i React Hooks są dość podobne.

Nasza funkcja React SimpleButton ma jedną ważną cechę w świecie programowania funkcyjnego: jest to czysta funkcja . Jeśli to czytasz, to przypuszczam, że przynajmniej raz słyszałeś ten termin. NgRx.io dwukrotnie cytuje czyste funkcje w kluczowych pojęciach:

  • Zmiany stanu są obsługiwane przez czyste funkcje zwane reducers , które pobierają bieżący stan i ostatnią akcję w celu obliczenia nowego stanu.
  • Selektory to czyste funkcje używane do wybierania, wyprowadzania i komponowania elementów stanu.

W React programiści są zachęcani do używania hooków jako czystych funkcji w jak największym stopniu. Angular zachęca również programistów do zaimplementowania tego samego wzorca przy użyciu paradygmatu Smart-Dumb Component.

Wtedy zdałem sobie sprawę, że brakuje mi pewnych kluczowych umiejętności programowania funkcjonalnego. Nie zajęło mi dużo czasu zrozumienie NgRx, ponieważ po zapoznaniu się z kluczowymi koncepcjami programowania funkcjonalnego, otrzymałem „Aha! moment”: Poprawiłem swoje zrozumienie NgRx i chciałem go bardziej wykorzystać, aby lepiej zrozumieć korzyści, jakie oferuje.

W tym artykule podzielę się moim doświadczeniem w nauce oraz zdobytą wiedzą na temat NgRx i programowania funkcjonalnego. Nie wyjaśniam API dla NgRx ani jak wywoływać akcje lub używać selektorów. Zamiast tego dzielę się, dlaczego zacząłem doceniać, że NgRx to świetna biblioteka: to nie tylko stosunkowo nowy trend, ale zapewnia wiele korzyści.

Zacznijmy od programowania funkcjonalnego .

Programowanie funkcjonalne

Programowanie funkcjonalne to paradygmat, który znacznie różni się od innych paradygmatów. To bardzo złożony temat z wieloma definicjami i wytycznymi. Jednak programowanie funkcjonalne zawiera kilka podstawowych pojęć, a ich znajomość jest warunkiem wstępnym do opanowania języka NgRx (i ogólnie JavaScript).

Te podstawowe pojęcia to:

  • Czysta funkcja
  • Niezmienny stan
  • Efekt uboczny

Powtarzam: to tylko paradygmat, nic więcej. Nie ma bibliotekifunction.js, którą pobieramy i używamy do pisania funkcjonalnego oprogramowania. To tylko sposób myślenia o pisaniu aplikacji. Zacznijmy od najważniejszego podstawowego pojęcia: czystej funkcji .

Czysta funkcja

Funkcja jest uważana za czystą funkcję, jeśli przestrzega dwóch prostych zasad:

  • Przekazywanie tych samych argumentów zawsze zwraca tę samą wartość
  • Brak obserwowalnego efektu ubocznego związanego z wykonaniem funkcji (zewnętrzna zmiana stanu, wywołanie operacji I/O itp.)

Czysta funkcja jest więc po prostu przezroczystą funkcją, która akceptuje niektóre argumenty (lub w ogóle nie ma argumentów) i zwraca oczekiwaną wartość. Masz pewność, że wywołanie tej funkcji nie spowoduje skutków ubocznych, takich jak połączenie sieciowe lub zmiana globalnego stanu użytkownika.

Przyjrzyjmy się trzem prostym przykładom:

 //Pure function function add(a,b){ return a + b; } //Impure function breaking rule 1 function random(){ return Math.random(); } //Impure function breaking rule 2 function sayHello(name){ console.log("Hello " + name); }
  • Pierwsza funkcja jest czysta, ponieważ przy przekazywaniu tych samych argumentów zawsze zwraca tę samą odpowiedź.
  • Druga funkcja nie jest czysta, ponieważ jest niedeterministyczna i przy każdym wywołaniu zwraca różne odpowiedzi.
  • Trzecia funkcja nie jest czysta, ponieważ wykorzystuje efekt uboczny (wywołanie console.log ).

Łatwo rozpoznać, czy funkcja jest czysta, czy nie. Dlaczego czysta funkcja jest lepsza niż nieczysta? Bo łatwiej o tym myśleć. Wyobraź sobie, że czytasz jakiś kod źródłowy i widzisz wywołanie funkcji, o której wiesz, że jest czyste. Jeśli nazwa funkcji jest prawidłowa, nie musisz jej badać; wiesz, że niczego nie zmienia, zwraca to, czego oczekujesz. Ma to kluczowe znaczenie dla debugowania, gdy masz dużą aplikację korporacyjną z dużą ilością logiki biznesowej, ponieważ może to być ogromna oszczędność czasu.

Ponadto łatwo go przetestować. Nie musisz niczego wstrzykiwać ani kpić z niektórych funkcji, po prostu przekazujesz argumenty i testujesz, czy wynik jest zgodny. Istnieje silny związek między testem a logiką: jeśli komponent jest łatwy do przetestowania, łatwo jest zrozumieć, jak i dlaczego działa.

Czyste funkcje są wyposażone w bardzo przydatną i przyjazną dla wydajności funkcję zwaną zapamiętywaniem. Jeśli wiemy, że wywołanie tych samych argumentów zwróci tę samą wartość, możemy po prostu buforować wyniki i nie tracić czasu na ponowne wywoływanie. NgRx zdecydowanie znajduje się na szczycie zapamiętywania; to jeden z głównych powodów, dla których jest szybki.

Transformacja powinna być intuicyjna i przejrzysta.

Możesz zadać sobie pytanie: „A co ze skutkami ubocznymi? Dokąd oni poszli?" Russ Olsen w swojej rozmowie GOTO żartuje, że nasi klienci nie płacą nam za czyste funkcje, płacą nam za skutki uboczne. To prawda: Nikogo nie obchodzi funkcja Calculator pure, jeśli nie zostanie ona gdzieś wydrukowana. Efekty uboczne mają swoje miejsce we wszechświecie programowania funkcjonalnego. Zobaczymy to wkrótce.

Na razie przejdźmy do następnego kroku w utrzymywaniu złożonych architektur aplikacji, kolejnej podstawowej koncepcji: niezmiennego stanu .

Niezmienny stan

Istnieje prosta definicja stanu niezmiennego:

  • Możesz tylko utworzyć lub usunąć stan. Nie możesz go zaktualizować.

Mówiąc prościej, aby zaktualizować wiek obiektu użytkownika… :

 let user = { username:"admin", age:28 }

… powinieneś napisać to tak:

 // Not like this newUser.age = 30; // But like this let newUser = {...user, age:29 }

Każda zmiana to nowy obiekt, który ma skopiowane właściwości ze starych. Jako tacy jesteśmy już w formie niezmiennego stanu.

String, Boolean i Number są stanami niezmiennymi: nie można dołączać ani modyfikować istniejących wartości. W przeciwieństwie do tego Date jest obiektem zmiennym: zawsze manipulujesz tym samym obiektem daty.

Niezmienność dotyczy całej aplikacji: jeśli przekażesz obiekt użytkownika wewnątrz funkcji, która zmienia jego wiek, nie powinna ona zmieniać obiektu użytkownika, powinna utworzyć nowy obiekt użytkownika ze zaktualizowanym wiekiem i zwrócić go:

 function updateAge(user, age) { return {...user, age: age) } let user = {username: 'admin', age: 29}; let newUser = updateAge(user, 32);

Dlaczego powinniśmy poświęcać temu czas i uwagę? Jest kilka korzyści, które warto podkreślić.

Jedną z zalet języków programowania zaplecza jest przetwarzanie równoległe. Jeśli zmiana stanu nie zależy od odwołania, a każda aktualizacja jest nowym obiektem, możesz podzielić proces na porcje i pracować nad tym samym zadaniem z niezliczoną liczbą wątków bez współdzielenia tej samej pamięci. Możesz nawet zrównoleglać zadania na serwerach.

W przypadku platform, takich jak Angular i React, przetwarzanie równoległe jest jednym z bardziej korzystnych sposobów poprawy wydajności aplikacji. Na przykład, Angular musi sprawdzić właściwości każdego obiektu, które przekazujesz przez powiązania danych wejściowych, aby rozpoznać, czy komponent ma być renderowany, czy nie. Ale jeśli ustawimy ChangeDetectionStrategy.OnPush zamiast wartości domyślnej, będzie sprawdzać według referencji, a nie według każdej właściwości. W dużej aplikacji zdecydowanie oszczędza to czas. Jeśli zaktualizujemy nasz stan w sposób niezmienny, otrzymamy ten wzrost wydajności za darmo.

Inna korzyść dla niezmiennego stanu, którą dzielą wszystkie języki programowania i frameworki, jest podobna do korzyści płynących z czystych funkcji: Łatwiej jest myśleć i testować. Kiedy zmiana jest nowym stanem zrodzonym ze starego, wiesz dokładnie, nad czym pracujesz i możesz dokładnie śledzić, jak i gdzie ten stan się zmienił. Nie tracisz historii aktualizacji i możesz cofnąć/ponowić zmiany stanu (przykładem jest React DevTools).

Jeśli jednak jeden stan zostanie zaktualizowany, nie poznasz historii tych zmian. Pomyśl o stanie niezmiennym, takim jak historia transakcji dla konta bankowego. To praktycznie pozycja obowiązkowa.

Teraz, gdy omówiliśmy niezmienność i czystość, zajmijmy się pozostałą podstawową koncepcją: efektem ubocznym .

Efekt uboczny

Możemy uogólnić definicję efektu ubocznego:

  • W informatyce mówi się, że operacja, funkcja lub wyrażenie mają efekt uboczny, jeśli modyfikują niektóre wartości zmiennych stanu poza środowiskiem lokalnym. To znaczy, że ma obserwowalny efekt oprócz zwracania wartości (główny efekt) wywołującemu operację.

Mówiąc najprościej, wszystko, co zmienia stan poza zakresem funkcji — wszystkie operacje we/wy i część pracy, która nie jest bezpośrednio połączona z funkcją — można uznać za efekt uboczny. Jednak musimy unikać stosowania efektów ubocznych w czystych funkcjach, ponieważ efekty uboczne są sprzeczne z filozofią programowania funkcjonalnego. Jeśli użyjesz operacji we/wy wewnątrz czystej funkcji, przestanie ona być czystą funkcją.

Niemniej jednak gdzieś trzeba mieć skutki uboczne, ponieważ aplikacja bez nich byłaby bezcelowa. W Angularze nie tylko czyste funkcje muszą być chronione przed skutkami ubocznymi, ale także musimy unikać ich używania w komponentach i dyrektywach.

Przyjrzyjmy się, jak możemy zaimplementować piękno tej techniki w ramach Angulara.

Ilustracja: kątowy efekt uboczny NgRx

Funkcjonalne programowanie kątowe

Jedną z pierwszych rzeczy, które należy zrozumieć w Angular, jest potrzeba jak najczęstszego rozdzielania komponentów na mniejsze komponenty, aby umożliwić łatwiejszą konserwację i testowanie. Jest to konieczne, ponieważ musimy podzielić naszą logikę biznesową. Ponadto programiści Angular są zachęcani do pozostawiania komponentów tylko do celów renderowania i przenoszenia całej logiki biznesowej wewnątrz usług.

Aby rozwinąć te koncepcje, użytkownicy Angulara dodali do swojego słownika wzorzec „Dumb-Smart Component”. Ten wzorzec wymaga, aby wywołania usług nie istniały w małych składnikach. Ponieważ logika biznesowa znajduje się w usługach, nadal musimy wywoływać te metody usługowe, czekać na ich odpowiedź, a dopiero potem dokonywać zmian stanu. Tak więc komponenty mają w sobie pewną logikę behawioralną.

Aby tego uniknąć, możemy stworzyć jeden Inteligentny Komponent (Komponent główny), który zawiera logikę biznesową i behawioralną, przekazuje stany przez Właściwości wejściowe i wywołuje akcje nasłuchujące parametrów wyjściowych. W ten sposób małe komponenty są naprawdę tylko do celów renderowania. Oczywiście nasz główny komponent musi zawierać pewne wywołania usług i nie możemy ich po prostu usunąć, ale jego użyteczność byłaby ograniczona tylko do logiki biznesowej, a nie renderowania.

Spójrzmy na przykład Counter Component. Licznik to składnik zawierający dwa przyciski zwiększające lub zmniejszające wartość oraz jeden displayField , który wyświetla wartość currentValue . Mamy więc cztery elementy:

  • Kontener Kontenerowy
  • Przycisk zwiększania
  • Przycisk zmniejszania
  • Aktualna wartość

Cała logika znajduje się w CounterContainer , więc wszystkie trzy są tylko rendererami. Oto kod dla trzech z nich:

 @Component({ selector: 'decrease-button', template: `<button (click)="increase.emit()" [disabled]="disabled"> Decrease </button>`, }) export class DecreaseButtonComponent { @Input() disabled!: boolean; @Output() increase = new EventEmitter(); } @Component({ selector: 'current-value', template: `<button> {{ currentValue }} </button>`, }) export class CurrentValueComponent { @Input() currentValue!: string; } @Component({ selector: 'increase-button', template: `<button (click)="increase.emit()" [disabled]="disabled"> Increase </button>`, }) export class IncreaseButtonComponent { @Input() disabled!: boolean; @Output() increase = new EventEmitter(); }

Spójrz, jakie są proste i czyste. Nie mają stanu ani skutków ubocznych, zależą tylko od właściwości wejściowych i emitowanych zdarzeń. Wyobraź sobie, jak łatwo je przetestować. Możemy nazwać je czystymi komponentami, bo tym naprawdę są. Zależą one tylko od parametrów wejściowych, nie mają skutków ubocznych i zawsze zwracają tę samą wartość (ciąg szablonu), przekazując te same parametry.

Tak więc czyste funkcje w programowaniu funkcjonalnym są przenoszone do czystych komponentów w Angularze. Ale gdzie idzie cała logika? Logika nadal istnieje, ale w nieco innym miejscu, a mianowicie w CounterComponent .

 @Component({ selector: 'counter-container', template: ` <decrease-button [disabled]="decreaseIsDisabled" (decrease)="decrease()"> </decrease-button> <current-value [currentValue]="currentValue"> </current-value> <increase-button (increase)="increase()" [disabled]="increaseIsDisabled"> </increase-button> `, }) export class CounterContainerComponent implements OnInit { @Input() disabled!: boolean; currentValue = 0; get decreaseIsDisabled() { return this.currentValue === 0; } get increaseIsDisabled() { return this.currentValue === 100; } constructor() {} ngOnInit(): void {} decrease() { this.currentValue -= 1; } increase() { this.currentValue += 1; } }

Jak widać, logika zachowania znajduje się w CounterContainer , ale brakuje części renderującej (deklaruje ona komponenty wewnątrz szablonu), ponieważ część renderująca dotyczy czystych komponentów.

Możemy wstrzyknąć tyle usług, ile chcemy, ponieważ tutaj obsługujemy wszystkie manipulacje danymi i zmiany stanu. Warto wspomnieć, że jeśli mamy głęboko zagnieżdżony komponent, nie możemy tworzyć tylko jednego komponentu na poziomie głównym. Możemy podzielić go na mniejsze sprytne elementy i zastosować ten sam wzór. Ostatecznie zależy to od złożoności i poziomu zagnieżdżenia dla każdego komponentu.

Możemy łatwo przeskoczyć z tego wzorca do samej biblioteki NgRx, która znajduje się tylko o jedną warstwę nad nią.

Biblioteka NgRx

Każdą aplikację webową możemy podzielić na trzy podstawowe części:

  • Logika biznesowa
  • Stan aplikacji
  • Logika renderowania

Ilustracja logiki biznesowej, stanu aplikacji i logiki renderowania.

Logika biznesowa to wszystkie zachowania zachodzące w aplikacji, takie jak praca w sieci, dane wejściowe, wyjściowe, API itp.

Stan aplikacji to stan aplikacji. Może być globalna, jako aktualnie autoryzowany użytkownik, a także lokalna, jako aktualna wartość Składnika Licznika.

Logika renderowania obejmuje renderowanie, takie jak wyświetlanie danych za pomocą DOM, tworzenie lub usuwanie elementów i tak dalej.

Używając wzorca Dumb-Smart oddzieliliśmy logikę renderowania od logiki biznesowej i stanu aplikacji, ale możemy je również podzielić, ponieważ oba są koncepcyjnie różne od siebie. Stan aplikacji jest jak migawka Twojej aplikacji w bieżącym czasie. Logika biznesowa jest jak statyczna funkcja, która jest zawsze obecna w Twojej aplikacji. Najważniejszym powodem ich podziału jest to, że Logika Biznesowa jest głównie efektem ubocznym, którego chcemy w jak największym stopniu uniknąć w kodzie aplikacji. Właśnie wtedy błyszczy biblioteka NgRx ze swoim funkcjonalnym paradygmatem.

Dzięki NgRx oddzielasz wszystkie te części. Istnieją trzy główne części:

  • Reduktory
  • działania
  • Selektory

W połączeniu z programowaniem funkcjonalnym, wszystkie te trzy elementy dają nam potężne narzędzie do obsługi aplikacji dowolnej wielkości. Przyjrzyjmy się każdemu z nich.

Reduktory

Reduktor to czysta funkcja, która ma prostą sygnaturę. Przyjmuje stary stan jako parametr i zwraca nowy stan, pochodzący ze starego lub nowego. Sam stan jest pojedynczym obiektem, który żyje wraz z cyklem życia aplikacji. To jak tag HTML, pojedynczy obiekt główny.

Nie możesz bezpośrednio modyfikować obiektu stanu, musisz go zmodyfikować za pomocą reduktorów. Ma to wiele zalet:

  • Logika zmiany stanu znajduje się w jednym miejscu, a Ty wiesz, gdzie i jak zmienia się stan.
  • Funkcje reduktora są czystymi funkcjami, łatwymi do testowania i zarządzania.
  • Ponieważ reduktory są czystymi funkcjami, można je zapamiętywać, co umożliwia ich buforowanie i unikanie dodatkowych obliczeń.
  • Zmiany stanu są niezmienne. Nigdy nie aktualizujesz tej samej instancji. Zamiast tego zawsze zwracasz nowy. Umożliwia to debugowanie „podróży w czasie”.

Oto trywialny przykład reduktora:

 function usernameReducer(oldState, username) { return {...oldState, username} }

Mimo, że jest to bardzo prosty atrapa reduktora, jest szkieletem wszystkich długich i skomplikowanych reduktorów. Wszyscy mają te same korzyści. Moglibyśmy mieć setki reduktorów w naszej aplikacji i możemy zrobić tyle, ile chcemy.

Dla naszego Counter Component, nasz stan i reduktory mogą wyglądać tak:

 interface State{ decreaseDisabled:boolean; increaseDisabled:boolean; currentValue:number; } const MIN_VALUE=0; const MAX_VALUE =100; function decreaseReducer(oldState) { const newValue = oldState.currentValue -1 return {...oldState,currentValue : newValue, decreaseDisabled: newValue===MIN_VALUE } function increaseReducer(oldState) { const newValue = oldState.currentValue + 1 return {...oldState,currentValue : newValue, decreaseDisabled: newValue===MAX_VALUE }

Usunęliśmy stan z komponentu. Teraz potrzebujemy sposobu na aktualizację naszego stanu i wywołanie odpowiedniego reduktora. Wtedy w grę wchodzą akcje.

działania

Akcja to sposób na powiadomienie NgRx o wywołaniu reduktora i zaktualizowaniu stanu. Bez tego użycie NgRx nie miałoby sensu. Akcja to prosty obiekt, który dołączamy do obecnego reduktora. Po jego wywołaniu zostanie wywołany odpowiedni reduktor, więc w naszym przykładzie możemy mieć następujące akcje:

 enum CounterActions { IncreaseValue = '[Counter Component] Increase Value', DecreaseValue = '[Counter Component] Decrease Value', } on(CounterActions.IncreaseValue,increaseReducer); on(CounterActions.DecreaseValue,decreaseReducer);

Nasze działania są dołączone do reduktorów. Teraz możemy dalej modyfikować nasz komponent kontenerowy i w razie potrzeby wywoływać odpowiednie akcje:

 @Component({ selector: 'counter-container', template: ` <decrease-button [disabled]="decreaseIsDisabled" (decrease)="decrease()"> </decrease-button> <current-value [currentValue]="currentValue"> </current-value> <increase-button (increase)="increase()" [disabled]="increaseIsDisabled"> </increase-button> `, }) export class CounterContainerComponent implements OnInit { constructor(private store: Store<any>) {} decrease() { this.store.dispatch(CounterActions.DicreaseValue); } increase() { this.store.dispatch(CounterActions.IncreaseValue); } }

Uwaga: usunęliśmy stan i wkrótce dodamy go ponownie .

Teraz nasz CounterContainer nie ma żadnej logiki zmiany stanu. Po prostu wie, co wysłać. Teraz potrzebujemy jakiegoś sposobu wyświetlania tych danych w widoku. To jest użyteczność selektorów.

Selektory

Selektor jest również bardzo prostą czystą funkcją, ale w przeciwieństwie do reduktora nie aktualizuje stanu. Jak sama nazwa wskazuje, selektor po prostu go wybiera. W naszym przykładzie moglibyśmy mieć trzy proste selektory:

 function selectCurrentValue(state) { return state.currentValue; } function selectDicreaseIsDisabled(state) { return state.decreaseDisabled; } function selectIncreaseIsDisabled(state) { return state.increaseDisabled; }

Używając tych selektorów, możemy wybrać każdy wycinek stanu w naszym inteligentnym komponencie CounterContainer .

 @Component({ selector: 'counter-container', template: ` <decrease-button [disabled]="ecreaseIsDisabled$ | async" (decrease)="decrease()" > </decrease-button> <current-value [currentValue]="currentValue$ | async"> </current-value> <increase-button (increase)="increase()" [disabled]="increaseIsDisabled$ | async" > </increase-button> `, }) export class CounterContainerComponent implements OnInit { decreaseIsDisabled$ = this.store.select(selectDicreaseIsDisabled); increaseIsDisabled$ = this.store.select(selectIncreaseIsDisabled); currentValue$ = this.store.select(selectCurrentValue); constructor(private store: Store<any>) {} decrease() { this.store.dispatch(CounterActions.DicreaseValue); } increase() { this.store.dispatch(CounterActions.IncreaseValue); } }

Te wybory są domyślnie asynchroniczne (podobnie jak ogólnie obserwowalne). Nie ma to znaczenia, przynajmniej z punktu widzenia wzorca. To samo dotyczyłoby synchronicznego, ponieważ po prostu wybieralibyśmy coś z naszego stanu.

Cofnijmy się i spójrzmy na szerszy obraz, aby zobaczyć, co osiągnęliśmy do tej pory. Mamy aplikację licznika, która składa się z trzech głównych części, które są prawie oddzielone od siebie. Nikt nie wie, w jaki sposób stan aplikacji zarządza sobą ani jak renderuje stan warstwa renderowania.

Odłączone części wykorzystują mostek (Akcje, Selektory) do łączenia się ze sobą. Są one oddzielone do tego stopnia, że ​​moglibyśmy wziąć cały kod State Application i przenieść go do innego projektu, na przykład do wersji mobilnej. Jedyne, co musielibyśmy zaimplementować, to renderowanie. Ale co z testowaniem?

Moim skromnym zdaniem testowanie jest najlepszą częścią NgRx. Testowanie tego przykładowego projektu jest podobne do gry w kółko i krzyżyk. Istnieją tylko czyste funkcje i czyste komponenty, więc ich testowanie to pestka. Teraz wyobraź sobie, że ten projekt stanie się większy, z setkami komponentów. Jeśli będziemy postępować zgodnie z tym samym wzorem, po prostu dołożymy do siebie coraz więcej kawałków. Nie stanie się bałaganem, nieczytelnym kleksem kodu źródłowego.

Prawie skończyliśmy. Pozostaje tylko jedna ważna rzecz do omówienia: skutki uboczne. Do tej pory wielokrotnie wspominałem o efektach ubocznych, ale przestałem wyjaśniać, gdzie je przechowywać.

A to dlatego, że efekty uboczne są wisienką na torcie i budując ten wzorzec, bardzo łatwo je usunąć z kodu aplikacji.

Skutki uboczne

Załóżmy, że nasza aplikacja licznika ma wbudowany zegar i co trzy sekundy automatycznie zwiększa wartość o jeden. To prosty efekt uboczny, który gdzieś musi żyć. Z definicji jest to ten sam efekt uboczny, co żądanie Ajax.

Jeśli myślimy o skutkach ubocznych, większość z nich ma dwa główne powody do istnienia:

  • Robienie czegokolwiek poza środowiskiem państwowym
  • Aktualizuję stan aplikacji

Na przykład przechowywanie jakiegoś stanu w LocalStorage jest pierwszą opcją, podczas gdy aktualizowanie stanu z odpowiedzi Ajax jest drugą. Ale oba mają ten sam podpis: każdy efekt uboczny musi mieć jakiś punkt wyjścia. Musi zostać wywołany przynajmniej raz, aby skłonić go do rozpoczęcia akcji.

Jak wspomnieliśmy wcześniej, NgRx ma fajne narzędzie do wydawania komuś polecenia. To jest akcja. Możemy wywołać każdy efekt uboczny, wysyłając akcję. Pseudokod mógłby wyglądać tak:

 function startTimer(){ setInterval(()=>{ console.log("Hello application"); },3000) } on(CounterActions.StartTime,startTimer) ... // We start timer by dispatching an action dispatch(CounterActions.StartTime);

To dość trywialne. Jak wspomniałem wcześniej, efekty uboczne albo coś aktualizują, albo nie. Jeśli efekt uboczny niczego nie aktualizuje, nie ma nic do zrobienia; po prostu to zostawiamy. Ale jeśli chcemy zaktualizować stan, jak to zrobić? W ten sam sposób, w jaki komponent próbuje zaktualizować stan: wywołując inną akcję. Więc nazywamy akcję wewnątrz efektu ubocznego, który aktualizuje stan:

 function startTimer(store) { setInterval(()=> { // We are dispatching another action dispatch(CounterActions.IncreaseValue) }, 3000) } on(CounterActions.StartTime, startTimer); ... // We start timer by dispatching an action dispatch(CounterActions.StartTime);

Mamy teraz w pełni funkcjonalną aplikację.

Podsumowując nasze doświadczenie NgRx

Zanim zakończymy naszą podróż z NgRx, chciałbym poruszyć kilka ważnych tematów:

  • Pokazany kod jest prostym pseudokodem, który wymyśliłem dla artykułu; nadaje się tylko do celów demonstracyjnych. NgRx to miejsce, w którym żyją prawdziwe źródła.
  • Nie ma oficjalnych wytycznych potwierdzających moją teorię dotyczącą łączenia programowania funkcjonalnego z biblioteką NgRx. To tylko moja opinia, która powstała po przeczytaniu dziesiątek artykułów i próbek kodu źródłowego stworzonych przez wysoko wykwalifikowanych ludzi.
  • Po użyciu NgRx na pewno zdasz sobie sprawę, że jest to o wiele bardziej złożone niż ten prosty przykład. Moim celem nie było uproszczenie tego, niż jest w rzeczywistości, ale pokazanie, że chociaż jest to nieco skomplikowane i może nawet skutkować dłuższą drogą do celu, jest warte dodatkowego wysiłku.
  • Najgorszym zastosowaniem NgRx jest używanie go wszędzie, niezależnie od rozmiaru lub złożoności aplikacji. W niektórych przypadkach nie należy używać NgRx; na przykład w formularzach. Zaimplementowanie formularzy w NgRx jest prawie niemożliwe. Formularze są przyklejone do samego DOM; nie mogą żyć osobno. Jeśli spróbujesz je rozdzielić, odkryjesz, że nienawidzisz nie tylko NgRx, ale ogólnie technologii internetowej.
  • Czasami użycie tego samego schematu kodu, nawet dla małego przykładu, może przerodzić się w koszmar, nawet jeśli może nam to przynieść korzyści w przyszłości. Jeśli tak jest, po prostu zintegruj się z inną niesamowitą biblioteką, która jest częścią ekosystemu NgRx (ComponentStore).