Flux i Redux: globalny store i jednokierunkowy przepływ danych

Ten wpis jest 24 częścią z 39 w kursie React.js
wp-content/uploads/2017/10/React_logo_wordmark-300x101-1-e1508612391308.png
  1. React.js: Wprowadzenie do kursu od podstaw
  2. Poznaj React.js
  3. Pierwszy komponent w React.js
  4. Props czyli atrybuty w React.js
  5. Podział na komponenty w React.js
  6. Klasy jako komponenty React.js
  7. Interakcja z komponentami React.js
  8. Stan komponentów React.js
  9. State w React.js 2
  10. Metody cyklu życia komponentu w React.js
  11. React.js w przykładach: filtrowanie statycznej listy
  12. Tworzenie aplikacji React.js dzięki create-react-app
  13. React.js na GitHub Pages dzięki create-react-app
  14. Testowanie aplikacji React.js — podstawy Enzyme
  15. Testowanie React.js w Enzyme — props, state i interakcje
  16. Poprawne bindowanie funkcji w React.js
  17. Odpowiadam na pytania: Babel, ECMAScript, destrukturyzacja, onClick, className
  18. Komunikacja pomiędzy komponentami w React.js
  19. Komunikacja z API w React.js
  20. Formularze w React.js — kontrolowane komponenty
  21. Formularze w React.js — niekontrolowane komponenty
  22. Odpowiadam na pytania: props, nawiasy klamrowe, funkcje vs klasy, import react
  23. TDD w React.js z pomocą react-testing-library
  24. Flux i Redux: globalny store i jednokierunkowy przepływ danych
  25. React + Redux — kurs: wprowadzenie i podstawy
  26. React + Redux — filtrowanie listy, proste selektory
  27. Projektowanie komponentów: Presentational & Container Components
  28. Asynchroniczność w Redux: redux-thunk
  29. Kiedy używać state, a kiedy Redux?
  30. Nowe metody cyklu życia: getDerivedStateFromProps i getSnapshotBeforeUpdate
  31. Leniwe ładowanie komponentów w React dzięki import
  32. Higher Order Reducers — Redux i powtarzanie kodu
  33. React Hooks — wprowadzenie i motywacja
  34. React Hooks: useState, czyli stan w komponentach funkcyjnych
  35. React Hooks: useState — wiele stanów, callbacki i inne niuanse
  36. React Hooks: useEffect — efekty uboczne w komponencie
  37. React Hooks a żądania do API
  38. useReducer — przenoszenie logiki poza komponent
  39. useMemo, useCallback, czyli rozwiązanie problemów ze zmieniającymi się propsami

Na codzień korzystam z Redux razem z React. Dawniej używałem też własnej implementacji architektury Flux razem z AngularJS. Postanowiłem napisać o tym koncepcie coś więcej — bo jest to bez wątpienia bardzo ciekawe! Jednokierunkowy przepływ danych, akcje, dispatcher, action creator, reducer… to wszystko w tym wpisie 🙂 A do tego klarowne, praktyczne przykłady!

Gdy opisywałem sposoby komunikacji pomiędzy kontrolerami w AngularJS, poniekąd celowo pominąłem pewną alternatywę, która zyskuje ostatnio sporą popularność: Architekturę Flux. Nie wspominałem o Fluksie głównie ze względu na to, że to koncept trochę szerszy niż prosta komunikacja pomiędzy elementami aplikacji o jakiej traktował tamten wpis.

Flux i Redux

Flux to angielskie słowo oznaczające strumień lub przepływ. Jest to też nazwa biblioteki architektury aplikacji zaproponowanej przez Facebooka. Gigant twierdzi zresztą, że używa Fluksa do budowania swoich aplikacji, a sam koncept stał się ostatnio niezwykle popularny. Podstawą Fluksa jest jeden wzorzec projektowy i jedno proste założenie, dlatego można zacząc z niego korzystać z niezwykłą wręcz łatwością, bez konieczności instalowania dodatkowych bibliotek czy frameworków. Naucz się Flux i Redux na naszym szkoleniu!

CQRS

Flux opiera się o stary, dobry wzorzec projektowy CQRS. Skrótowiec rozwija się do Command Query Responsibility Segregation, czyli w luźnym tłumaczeniu rozdzielenie zapytań od rozkazów. Wzorzec ten został pierwszy raz opisany przez Bertranda Meyera i spopularyzowany przez Grega Younga, i zasadniczo opiera się o pomysł, aby rozdzielić od siebie fragmenty modelu odpowiedzialne za pobieranie informacji od tych odpowiedzialnych za ich modyfikację.

Dlaczego ma to sens? Jednym z często powtarzanych przykładów jest różnica w częstotliwości odczytywania i zapisywania czegoś przez użytkowników w aplikacji. Wyobraź sobie portal społecznościowy, na którym każdy może czytać posty innych oraz samemu wrzucać nowe ciekawostki ze swojego życia. Zgodnie z zasadą Pareta, nie pomylisz się bardzo jeśli założysz, że 80% postów pochodzi od 20% użytkowników. Innymi słowy tylko 20% użytkowników coś pisze, a reszta prawie wyłącznie czyta.

Na tej podstawie warto zadać pytanie: Czy sensownie jest, aby fragmenty aplikacji odpowiedzialne za pisanie i za czytanie były połączone i skalowane w tym samym stopniu? Prawdopodobnie nie i prawdopodobnie w analogiczny sposób zasadę Pareta można zastosować z powodzeniem do dowolnej aplikacji internetowej. Jednym z założeń wzorca CQRS jest rozwiązanie tego problemu. Warto też jednak pamiętać, że stosowanie CQRS nie zawsze ma sens – Martin Fowler pisze więcej na ten temat na swoim blogu.

Podstawy Flux i Redux

Architektura Flux przewiduje istnienie trzech głównych części1:

  • Dispatcher – odpowiedzialny jest za odbieranie akcji i rozsyłanie ich do odpowiednich Store’ów
  • Store – odpowiadają za przechowywanie informacji
  • View – widok, źródło akcji

Dodatkowym założeniem obowiązującym we Fluksie jest to, że informacje przepływają tylko w jednym, zawsze tym samym kierunku. Flux rezygnuje z MVC na rzecz jednokierunkowego przepływu danych (unidirectional data flow). Istotna jest tutaj całkowita enkapsulacja Store’ów – z zewnątrz można przy pomocy odpowiedniej metody jedynie odczytać zawartość Store’a, niemożliwa jest jednak jego modyfikacja. Wszelkie zmiany jego zawartości zachodzą poprzez przesłanie odpowiedniej akcji przez Dispatcher. I w ten sposób jasny staje się przepływ informacji2:

Architektura Flux

Tak to wygląda koncepcyjnie. Więcej na ten temat można poczytać na stronie Fluksa. Jest to tylko architektura, a więc implementacja tego pomysłu może być w zasadzie dowolna (np. Redux czy MobX). Facebook udostępnia co prawda swoją bibliotekę Dispatcher.js, ale nie trzeba z niej wcale korzystać. Implementacji Fluksa jest sporo, ostatni raz gdy sprawdzałem było ich chyba tuzin, niekompletną listę można znaleźć tutaj. O Fluksie szerzej opowiadał na Gdańskim meet.js Łukasz Kużyński. Dalej chciałbym powiedzieć o całkowicie alternatywnej implementacji Fluksa – Redux.

Redux

Redux jest implementacją architektury Flux, do której dodano nieco programowania funkcyjnego i skorzystano ze wzorca Event Sourcing. Na stronie Reduksa podane są trzy główne zasady, o której opiera się cała filozofia tej biblioteki:

Kurs Flux i Redux
Logo Redux

Cały stan aplikacji jest przechowywany w drzewie w jednym storze

To założenie sprawia, że rozumowanie na temat stanu aplikacji staje się jeszcze prostsze. Chcesz przesłać stan z serwera do aplikacji lub w drugą stronę? Nie ma najmniejszego problemu, to tylko jeden obiekt. Chcesz zserializować stan i zapisać np. do JSON? Nic prostszego.

Stan jest tylko do odczytu; wszystkie zmiany zachodzą poprzez akcje

Nic nie modyfikuje stanu bezpośrednio. Zamiast tego, wysyłane są akcje, które reprezentują intencje. Wszystkie akcje przechodzą przez centralny punkt i są analizowane jedna po jednej w określonej kolejności (reducer). Dzięki temu nie tylko eliminowane są wszelkie wyścigi, ale też możliwe jest np. zapisywanie zdarzeń w celu łatwiejszego debugowania. Jest to nic innego niż implementacja wzorca Event Sourcing i dzięki temu trywialna stała się implementacja funkcji, które do tej pory były niezwykle skomplikowane: na przykład „cofnij” i „powtórz” – który zresztą jest sztandarowym przykładem Reduksa.

Aby zdefiniować jak akcja wpływa na stan, należy napisać reducer, który jest pure function3

Reducer to funkcja, które przyjmuje poprzedni stan oraz akcję i zwraca zupełnie nowy stan, nie zmieniając przy okazji obiektu reprezentującego stanu poprzedni (nie mutują go). Najczęściej rozpoczyna się od stworzenia jednego reducera, a później, gdy aplikacja się rozrasta, dodaje się kolejne reducery, znajmujące się konkretnymi fragmentami stanu.

Wydaje się proste? No i jest bardzo proste. To w zasadzie wszystko co trzeba wiedzieć o Reduksie! Trzy zasady wraz z przykładami kody można znaleźć w dokumentacji Reduksa. Przejdźmy teraz do konkretów…

Aplikacja z Redux

Najprostszym i powszechnie powielanym przykładem użycia Redux jest stworzenie aplikacji-licznika, której jedynym zadaniem jest reagowanie na kliknięcia w guziki, które zwiększają i zmniejszają licznik wyświetlany na stronie. Łatwizna! Spójrz na kod źródłowy. To, co Cię interesuje to funkcja reducer oraz stworzenie Store’a:

// pure reducer function
function counter(state = 0, action) {  
    switch (action.type) {
        case 'INCREMENT':
            return ++state;
        case 'DECREMENT':
            return --state;
    }
    return state;
}

W pierwszej linijce definiuję, że domyślnie state = 0, gdy ten argument nie będzie zdefiniowany. Może się tak zdarzyć, jeśli jest to stan początkowy i w takim wypadku to reducer powinien wiedzieć jaki jest domyślny stan aplikacji. Przypisuję więc 0, bo to od tej liczby chciałbym zacząć liczyć. Następnie sprawdzam jaka akcja miała miejsce. Zwyczajowo akcje w Reduksie są zwykłymi stringami, więc prosty switch wystarczy. Odpowiednio zwiększam lub zmniejszam licznik i zwracam nowy stan.

const store = Redux.createStore(counter);  
store.subscribe(render);  
render();  

Następnie tworzę nowy store oraz wywołuję funkcję store.subscribe(render), dzięki której render zostanie automatycznie wywołany zawsze, gdzy store się zmieni. Wewnątrz funkcji render pobieram zaś zawartość store’a i wyświetlam w najprotszy możliwy sposób:

function render() {  
    $$('#result').textContent = store.getState();
}

Ostatecznie podpinam pod zdarzenia click obu przycisków akcje INCREMENTDECREMENT:

store.dispatch({  
    type: 'INCREMENT'
});

Od teraz po kliknięciu przycisków licznik się zmienia. Zobacz to na własne oczy:

Zobacz Pen xOZWYy – Michał Miszczyszyn (@mmiszy) na CodePen.

Niemutowalny stan w Redux

Wspomniałem o tym, że stan zwracany przez reducer musi być całkowicie nowym obiektem – nie można mutować poprzedniego stanu. W prostym przykładzie powyżej nie było problemu, ponieważ stanem była liczba, czyli jeden z typów prostych, które w JavaScripcie przekazywane są przez wartość (kopiowane).

Weź jednak bardziej skomplikowany przykład. Przyjmijmy, że stan nie jest liczbą, lecz obiektem state = {counter: 0}. W takim przypadku musisz zwrócić całkowicie nowy obiekt z powiększonym lub zmniejszonym licznikiem. Z pomocą przychodzi funkcja Object.assign:

function reducer(state = {counter: 0}, action) {  
    switch (action.type) {
        case 'INCREMENT':
            return Object.assign({}, state, {counter: state.counter + 1});
        case 'DECREMENT':
            return Object.assign({}, state, {counter: state.counter - 1});
    }
    return state;
}

Object.assign({}, state) oznacza tyle co „skopiuj pola z obiektu state do nowego pustego obiektu”. Kolejne argumenty przekazywane do tej funkcji powodują dodanie lub nadpisanie odpowiednich pól w obiekcie. Więcej na ten temat można doczytać w artykule Object.assign na MDN. Jest to niezwykle przydatna funkcja, gdy zależy nam na szybkim płytkim skopiowaniu jakiejś struktury danych. Analogiczne rozwiązania dostępne są też w popularnych bibliotekach takich jak lodash albo underscore.

Zobacz Pen dXGmax – Michał Miszczyszyn (@mmiszy) na CodePen.

Podsumowanie Flux i Redux

To w zasadzie wszystko, co chciałem dzisiaj napisać. Celowo wydzieliłem posta o Fluksie i Reduksie, bo to architektura bardzo uniwersalna i niezwiązana właściwie z żadnym frameworkiem. Znajomość uniwersalnych wzorców jest zawsze bardziej cenna niż znajomość konkretnych frameworków.

W tym wpisie omówiłem podstawowe założenia architektury Flux oraz jej zalety. Opisałem też jedną przykładową, bardzo popularną implementację o nazwie Redux. Zachęcam do komentowania!

  1. Tłumaczenie Dispatcher jako dyspozytor wydało mi się komiczne, więc pozostanę przy oryginalnych angielskich nazwach.
  2. Obrazek z https://facebook.github.io/flux/docs/overview.html#content
  3. Teoretycznie to określenie funkcjonuje w języku polskim jako „czysta funkcja”, ale nie brzmi to dla mnie dobrze
Nawigacja po kursie:
  1. React.js: Wprowadzenie do kursu od podstaw
  2. Poznaj React.js
  3. Pierwszy komponent w React.js
  4. Props czyli atrybuty w React.js
  5. Podział na komponenty w React.js
  6. Klasy jako komponenty React.js
  7. Interakcja z komponentami React.js
  8. Stan komponentów React.js
  9. State w React.js 2
  10. Metody cyklu życia komponentu w React.js
  11. React.js w przykładach: filtrowanie statycznej listy
  12. Tworzenie aplikacji React.js dzięki create-react-app
  13. React.js na GitHub Pages dzięki create-react-app
  14. Testowanie aplikacji React.js — podstawy Enzyme
  15. Testowanie React.js w Enzyme — props, state i interakcje
  16. Poprawne bindowanie funkcji w React.js
  17. Odpowiadam na pytania: Babel, ECMAScript, destrukturyzacja, onClick, className
  18. Komunikacja pomiędzy komponentami w React.js
  19. Komunikacja z API w React.js
  20. Formularze w React.js — kontrolowane komponenty
  21. Formularze w React.js — niekontrolowane komponenty
  22. Odpowiadam na pytania: props, nawiasy klamrowe, funkcje vs klasy, import react
  23. TDD w React.js z pomocą react-testing-library
  24. Flux i Redux: globalny store i jednokierunkowy przepływ danych
  25. React + Redux — kurs: wprowadzenie i podstawy
  26. React + Redux — filtrowanie listy, proste selektory
  27. Projektowanie komponentów: Presentational & Container Components
  28. Asynchroniczność w Redux: redux-thunk
  29. Kiedy używać state, a kiedy Redux?
  30. Nowe metody cyklu życia: getDerivedStateFromProps i getSnapshotBeforeUpdate
  31. Leniwe ładowanie komponentów w React dzięki import
  32. Higher Order Reducers — Redux i powtarzanie kodu
  33. React Hooks — wprowadzenie i motywacja
  34. React Hooks: useState, czyli stan w komponentach funkcyjnych
  35. React Hooks: useState — wiele stanów, callbacki i inne niuanse
  36. React Hooks: useEffect — efekty uboczne w komponencie
  37. React Hooks a żądania do API
  38. useReducer — przenoszenie logiki poza komponent
  39. useMemo, useCallback, czyli rozwiązanie problemów ze zmieniającymi się propsami