Skocz do treści

Już wkrótce odpalamy zapisy na drugą edycję next13masters.pl. Zapisz się na listę oczekujących!

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

Na co dzień 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!

Ten artykuł jest częścią 23 z 42 w serii React.js.

Zdjęcie Michał Miszczyszyn
Dobry Kod13 komentarzy

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ąć z niego korzystać z niezwykłą wręcz łatwością, bez konieczności instalowania dodatkowych bibliotek czy frameworków. zapisz się na szkolenie z Flux i Redux.

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, zajmują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, gdy store się zmieni. Wewnątrz funkcji render pobieram zaś zawartość store’a i wyświetlam w najprostszy możliwy sposób:

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

Ostatecznie podpinam pod zdarzenia click obu przycisków akcje INCREMENT i DECREMENT:

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

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

Zobacz 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 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

👉  Znalazłeś/aś błąd?  👈Edytuj ten wpis na GitHubie!

Autor