- React.js: Wprowadzenie do kursu od podstaw
- Poznaj React.js
- Pierwszy komponent w React.js
- Props czyli atrybuty w React.js
- Podział na komponenty w React.js
- Klasy jako komponenty React.js
- Interakcja z komponentami React.js
- Stan komponentów React.js
- State w React.js 2
- Metody cyklu życia komponentu w React.js
- React.js w przykładach: filtrowanie statycznej listy
- Tworzenie aplikacji React.js dzięki create-react-app
- React.js na GitHub Pages dzięki create-react-app
- Testowanie aplikacji React.js — podstawy Enzyme
- Testowanie React.js w Enzyme — props, state i interakcje
- Poprawne bindowanie funkcji w React.js
- Odpowiadam na pytania: Babel, ECMAScript, destrukturyzacja, onClick, className
- Komunikacja pomiędzy komponentami w React.js
- Komunikacja z API w React.js
- Formularze w React.js — kontrolowane komponenty
- Formularze w React.js — niekontrolowane komponenty
- Odpowiadam na pytania: props, nawiasy klamrowe, funkcje vs klasy, import react
- TDD w React.js z pomocą react-testing-library
- Flux i Redux: globalny store i jednokierunkowy przepływ danych
- React + Redux — kurs: wprowadzenie i podstawy
- React + Redux — filtrowanie listy, proste selektory
- Projektowanie komponentów: Presentational & Container Components
- Asynchroniczność w Redux: redux-thunk
- Kiedy używać state, a kiedy Redux?
- Nowe metody cyklu życia: getDerivedStateFromProps i getSnapshotBeforeUpdate
- Leniwe ładowanie komponentów w React dzięki import
- Higher Order Reducers — Redux i powtarzanie kodu
- React Hooks — wprowadzenie i motywacja
- React Hooks: useState, czyli stan w komponentach funkcyjnych
- React Hooks: useState — wiele stanów, callbacki i inne niuanse
- React Hooks: useEffect — efekty uboczne w komponencie
- React Hooks a żądania do API
- useReducer — przenoszenie logiki poza komponent
- useMemo, useCallback, czyli rozwiązanie problemów ze zmieniającymi się propsami
- Wady React Hooks
- React Hooks: Piszemy własne hooki!
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. Poznaj Flux i Redux w dwa dni na 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:
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


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 INCREMENT
i DECREMENT
:
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!
- Tłumaczenie Dispatcher jako dyspozytor wydało mi się komiczne, więc pozostanę przy oryginalnych angielskich nazwach. ↩
- Obrazek z https://facebook.github.io/flux/docs/overview.html#content ↩
- Teoretycznie to określenie funkcjonuje w języku polskim jako „czysta funkcja”, ale nie brzmi to dla mnie dobrze ↩
- React.js: Wprowadzenie do kursu od podstaw
- Poznaj React.js
- Pierwszy komponent w React.js
- Props czyli atrybuty w React.js
- Podział na komponenty w React.js
- Klasy jako komponenty React.js
- Interakcja z komponentami React.js
- Stan komponentów React.js
- State w React.js 2
- Metody cyklu życia komponentu w React.js
- React.js w przykładach: filtrowanie statycznej listy
- Tworzenie aplikacji React.js dzięki create-react-app
- React.js na GitHub Pages dzięki create-react-app
- Testowanie aplikacji React.js — podstawy Enzyme
- Testowanie React.js w Enzyme — props, state i interakcje
- Poprawne bindowanie funkcji w React.js
- Odpowiadam na pytania: Babel, ECMAScript, destrukturyzacja, onClick, className
- Komunikacja pomiędzy komponentami w React.js
- Komunikacja z API w React.js
- Formularze w React.js — kontrolowane komponenty
- Formularze w React.js — niekontrolowane komponenty
- Odpowiadam na pytania: props, nawiasy klamrowe, funkcje vs klasy, import react
- TDD w React.js z pomocą react-testing-library
- Flux i Redux: globalny store i jednokierunkowy przepływ danych
- React + Redux — kurs: wprowadzenie i podstawy
- React + Redux — filtrowanie listy, proste selektory
- Projektowanie komponentów: Presentational & Container Components
- Asynchroniczność w Redux: redux-thunk
- Kiedy używać state, a kiedy Redux?
- Nowe metody cyklu życia: getDerivedStateFromProps i getSnapshotBeforeUpdate
- Leniwe ładowanie komponentów w React dzięki import
- Higher Order Reducers — Redux i powtarzanie kodu
- React Hooks — wprowadzenie i motywacja
- React Hooks: useState, czyli stan w komponentach funkcyjnych
- React Hooks: useState — wiele stanów, callbacki i inne niuanse
- React Hooks: useEffect — efekty uboczne w komponencie
- React Hooks a żądania do API
- useReducer — przenoszenie logiki poza komponent
- useMemo, useCallback, czyli rozwiązanie problemów ze zmieniającymi się propsami
- Wady React Hooks
- React Hooks: Piszemy własne hooki!
Cześć!
Chciałem jedynie napisać, że bardzo mi przypadły do gustu Twoje artykuły! Pomimo tego, że dotyczą one, z pozoru, tylko Angulara, to moim zdaniem są uniwersalne. Odniesienia do wzorców projektowych zachęcają do dalszych poszukiwań i nauki. 👍
Pozdrawiam,
Artur
Starałem się, aby ten był możliwie uniwersalny, ale musiałem wspomnieć o Angularze bo głównie z nim pracuję 🙂 Sednem jest zrozumienie tego, że konkretne frameworki są chwilową modą, a wzorce projektowe są uniwersalne i na zawsze!
Aktualnie pracujesz na Angularze 1.x czy dwójce? Jeśli jedynce to czy planujesz przejście na Angulara 2?
Pozdrawiam 🙂
Pracowałem nad dwoma projektami w Angular 2 i 5, ale głównie pracuję już z React. Przywiązywanie się do jednej technologii na dłużej do niczego nie prowadzi — trzeba poznawać uniwersalne wzorce projektowe i dobre praktyki 🙂
Komercyjnie tylko w AngularJS 1.x. Angular 2 jest moim zdaniem jeszcze zbyt młody, żeby go pchać do prawdziwych aplikacji – choćby z tego względu, że w ciągu ostatnich kilku tygodni całkowicie zmienion router i obsługę formularzy.
„rozdzielić od siebie fragmenty modelu odpowiedzialne za pobieranie informacji od tych odpowiedzialnych za zapisywanie danych” – nie zapisywanie danych, a modyfikację stanu 🙂
Martin napisał „[the heart of CQRS] is the notion that you can use a different model to update information than the model you use to read information”
Właśnie chodzi o modyfikację stanu. „Zapisywanie danych” sugeruje fizyczne zapisanie jakichś danych. „Information update” natomiast to dosłownie uaktualnienie informacji, czyli zmodyfikowanie stanu (obiektów / danych na dysku). Przykładowo, komenda AddPrivateMessage mogłaby wykonać userObject.addPrivateMessage(messageObject). To tak jeżeli mogę dodać od siebie jako, że zawodowo pracuję w backendzie 🙂
Może masz rację, ale nie chodziło mi absolutnie o zapisanie danych fizycznie 😛 Zmienię trochę to sformułowanie 😉 Dzięki.
[…] oraz przesyłania danych pomiędzy odległymi komponentami zastosowałbym raczej bibliotekę Redux. Koncept Reduksa już opisywałem, a jego konkretne zastosowanie w aplikacji Angular 2 znajdzie się w kolejnym wpisie, który już […]
[…] pomiędzy komponentami. Na czym dokładnie polega filozofia Reduksa opisywałem we wpisie Flux i Redux, więc osoby nieznające tego konceptu zapraszam do tamtego wpisu. W tym artykule chciałbym […]
[…] z wykorzystaniem Reduksa. Aby oswoić się z samymi konceptami Reduksa, polecam mój wpis Flux i Redux. Zachęcam do […]
[…] Flux i Redux […]
Drobny szczegół z teorii się tutaj nie zgadza. Meyer wymyślił CQS (Command query separation), który dotyczy rozdzielenia komend i zapytań na poziomie interfejsu obiekt w celu ułatwienia zrozumienia kodu (po prostu dobrze wiedzieć, że dana metoda nie ma side effectów i jest idempotentna). Greg Young natomiast rozszerzył ten wzorzec na scope usług (CQRS), ale wprowadził to pojęcie z trochę innych pobudek, mianowicie aby oddzielić logikę domenową serwisów-komend (często bardzo skomplikowaną, gdzie performance nie ma dużego znaczenia) i zapytań (gdzie logika jest trywialna, ale za to zależy nam na szybkości).
Wait, to jak mam edytować ten akapit? 😀
Słusznie, słusznie!