Polub bloga na fejsie!

Usprawnienia Redux – zróbmy to trochę lepiej

23

W poprzednim wpisie przedstawiłem Ci absolutne podstawy Redux. Dowiedziałeś się z niego na czym polega architektura Flux oraz przedstawiłem Ci podstawy jednej z najlepszych jego implementacji. Na końcu tamtego artykułu napisałem, że w kolejnym wpisie przedstawię trochę ulepszeń dla Redux. Nie lubię rzucać słów na wiatr, dlatego też dziś przedstawiam wpis, w którym pokażę różne usprawnienia Redux, które spowodują, że praca z aplikacją ReactJS będzie łatwiejsza… Myślę, że nie ma co przedłużać tego wstępu – przejdźmy więc do rzeczy!

P.S. Do pełnego zrozumienia tego wpisu potrzebna jest wcześniejsza znajomość Reduxa. Jeśli nie do końca wiesz co to jest, przeczytaj najpierw mój wpis na temat podstaw Redux!

Immutable.js – najlepszy sposób na niezmienny stan Redux

Myślę, że najlepiej będzie najważniejsze usprawnienia Redux opisać na początku…

Biblioteka Immutable.js to kolejny projekt open-source od Facebooka.  Jak sama nazwa wskazuje służy ona do zapewniania niezmienności danych. To wzbudza podejrzenie, że idealnie będzie ona współgrać z Reduxem, w którym przecież stan aplikacji również powinien być obiektem niezmiennym. Zresztą za chwilę się o tym przekonasz.

Jeszcze raz… co to jest Immutable.js?

Dla porządku, poniżej przedstawiam cytat z oficjalnej dokumentacji Immutable.js, który objaśnia czym tak na prawdę jest ta biblioteka:

Immutable data cannot be changed once created, leading to much simpler application development, no defensive copying, and enabling advanced memoization and change detection techniques with simple logic. Persistent data presents a mutative API which does not update the data in-place, but instead always yields new updated data.

Immutable.js provides many Persistent Immutable data structures including: List, Stack, Map, OrderedMap, Set, OrderedSet and Record.

Jak więc widzisz, dane niezmienne (ang. immutable) nie mogą być już modyfikowane po ich utworzeniu. Wpływa to korzystnie na uproszczenie procesu developmentu oraz pozwala na wprowadzenie technik wykrywania zmian, które posiadają prostą logikę. Niezmienne obiekty Immutable.js posiadają specjalne API, które pozwala na manipulację danymi. Jednak same dane nie ulegają zmianie, są przecież niezmienne. Zamiast tego wywołanie metody API,  na przykład set, powoduje zwrócenie nowej wersji niezmiennego obiektu, która zawiera wymagane zmiany.

Przykład wykorzystania Immutable.js

Hmm… brzmi jak coś co idealnie przyda się w Redux, prawda? Myślę, ze czas przejść do zaprezentowania przykładu kodu z użyciem biblioteki Immutable.js (na razie bez kontekstu Reduxa):

W powyższym przykładzie użyłem obiektu Record należącego do biblioteki Immutable.js. Struktura Record jest nieposortowaną kolekcją par klucz-wartość. To nie przypadek, że użyłem właśnie tego obiektu. Użyjemy go w dalszej części artykułu, gdzie przedyskutujemy wykorzystanie Immutable.JS razem z Reduxem.

Najważniejsze w zaprezentowanym przykładzie są linie 4 oraz 5. Jak widzisz, wykorzystałem wspomnianą przed chwilą metodę set, należącą do API Immutable.js, do zmiany wartości key na 2 (linia czwarta). W kolejnej linii możesz zauważyć, że obiekt value nie uległ zmianie – wartość jego właściwości key wciąż wynosi 1.

Jeśli teraz spojrzysz na linię numer 7 zauważysz, że w tym przypadku przypisuję rezultat wywołania metody set do obiektu value. Tak jak wspomniałem wcześniej, metody API nie zmieniają obiektu. Zamiast tego zwracają jego nową wersję dlatego też takie ponowne przypisanie jest konieczne. Na dowód, w linii numer 8 sprawdzam wartość klucza po ponownym przypisaniu obiektu value. Jak widzisz, zgodnie z moją intencją tym razem jest to wartość 2.

Myślę, że ten przykład wystarczy by zrozumieć zasadę działania biblioteki Immutable.js. Inne dostępne w niej struktury danych działają na tej samej zasadzie. Czas wreszcie na jakieś usprawnienia Redux – pierwszym z nich będzie użycie Immutable.js!

Usprawnienia Redux – stan aplikacji jako obiekt Immutable.js

No dobra, przejdźmy do sedna. Jak już pisałem w poprzednim moim artykule, a także wspominałem wcześniej tutaj, stan aplikacji w Redux powinien być niezmienny. Za każdym razem gdy funkcja reducer musi zmienić stan, tak na prawdę zwraca ona nowy obiekt stanu, który zawiera niezbędne modyfikacje. Aby ten proces ulepszyć warto wykorzystać opisywaną przeze mnie bibliotekę Immutable.js.

Myślę, że najlepiej będzie pokazać to po prostu na przykładzie. Spójrzmy najpierw na reducer z mojego poprzedniego wpisu:

Teraz możemy to bardzo łatwo przepisać na użycie Immutable.js:

POLUB BLOGA NA FACEBOOKU!

Chcesz być na bieżąco informowany o nowościach na blogu oraz innych ciekawych treściach? Polub fanpage bloga na Facebooku!

Przeanalizujmy powyższy przykład… W pierwszej linii definiuję nową zmienną – initialState. Do zmiennej tej przypisuję strukturę danych Immutable.Record. Dodatkowo inicjuję ją od razu obiektem, który posiada właściwość counter o wartości 0. Rzuć teraz okiem na ostatnią linię przykładu. Obiekt ten przekazuję do metody createStore jako jej drugi parametr. Od tego momentu stan aplikacji jest obiektem Record.

To co jeszcze jest tutaj ważne to to, że teraz aby dokonać zmiany stanu aplikacji możemy, czy raczej jesteśmy zmuszeni, użyć API Immutable.js. W liniach numer 6 oraz 8 widać użycie metody set API. Jak już wiemy to spowoduje zwrócenie nowej, zaktualizowanej wersji obiektu stanu.

Uzyskaliśmy więc dokładnie to samo co w poprzednim przykładzie tyle, że z użyciem bezpieczniejszego podejścia. Dzięki Immutable.js stan nie może zostać przypadkowo zmieniony – aby to zrobić zawsze jesteś zmuszony użyć API.

I to wszystko! Pierwsze usprawnienia Redux za nami! Proste prawda?

Reducery i akcje w wielu plikach

Ok, czas teraz pokazać kolejne usprawnienia Redux. Z poprzedniego wpisu na temat podstaw Redux dowiedziałeś się, że obowiązuje nas jedno jedyne źródło prawdy czyli, że „stan całej aplikacji przetrzymywany jest w drzewie obiektów wewnątrz pojedynczego obiektu store„. Nie znaczy to jednak, że musimy koniecznie mieć jeden wielki reducer i trzymać go w jednym pliku. Podobnie sprawa ma się w przypadku kreatorów akcji. Dobrą praktyką jest rozbić je sobie na mniejsze pliki, najczęściej odpowiadające poszczególnym obszarom aplikacji.

Rozbicie reducerów na mniejsze funkcje

Jak zawsze u mnie bywa, najlepiej jest przedstawić przykład a później go omówić. Załóżmy, że mamy poniższy reducer znany już z poprzedniego wpisu:

Oczywiście jest już on usprawniony za pomocą Immutable.js. Przyjmijmy teraz, że w naszej aplikacji mamy inny obszar, na przykład inną podstronę w routingu. Moglibyśmy chcieć wydzielić dla niej osobny reducer:

Zwróć uwagę, że zarówno w jednym jak i drugim przypadku, parametr state funkcji reducera jest teraz inicjowany stałą initialState. Za chwilę dowiesz się, dlaczego robię to w ten właśnie sposób.

Łączenie reducerów

No dobra, wszystko fajnie ale przecież funkcja createStore przyjmuje jako parametr tylko jeden reducer tak? W zasadzie to tak, w poprzednim wpisie pokazywałem przecież taki przykład tworzenia obiektu store:

Na szczęście Redux przychodzi nam z pomocą dostarczając nam specjalną funkcję combineReducers, która pozwala na połączenie naszych „reducerów” w jeden obiekt. Zostanie on następnie prawidłowo obsłużony przez funkcję createStore:

Jak widzisz, funkcję combineReducers importuję z pakietu redux. Wywołując ją przekazuję mu obiekt zawierający właściwości, do których przypisuję poszczególne „reducery”. Zwróć uwagę na zapis bez dwukropka – jest to nowy skrócony zapis dostępny w ES6. Powoduje on utworzenie właściwości o tej samej nazwie co wartość do niej przypisywana z jednoczesnym jej przypisaniem.

Dodatkowo zwróć też uwagę na ostatnią linię przykładu. Jak widzisz, jako stan początkowy przekazuję pusty obiekt. To dlatego, że teraz mam stan rozrzucony po kilku plikach. Nie jest to jednak problem ponieważ każda część stanu jest inicjowana w poszczególnych plikach/modułach reducerów w momencie utworzenia obiektu Immutable.js – przypisanie state = initialState, o którym wcześniej wspomniałem. Zgodnie z tym jak działają moduły ES6 każda z tych inicjalizacji nastąpi w momencie załadowania danego modułu.

Grupowanie kreatorów akcji w osobnych plikach

Nie tylko „reducery” można rozbijać na mniejsze moduły. To samo można zrobić z kreatorami akcji. Zwykle stosuje się podejście, w którym dla każdego modułu „reducera” tworzy się osobny moduł zawierający kreatory akcji. W przypadku przedstawionych powyżej dwóch „reducerów” mielibyśmy następujące moduły kreatorów akcji:

… oraz …

W powyższych przykładach zwróć uwagę, że eksportuję również stałe, których wartości przypisywane są do właściwości type zwracanych obiektów. Jest to dobra praktyka, aby posługiwać się w tym przypadku stałymi. Można wtedy uniknąć literówek. Popularną praktyką jest trzymanie wszystkich tych stałych w osobnym pliku consts. Dzięki temu unikniesz przypadkowego utworzenia dwóch stałych o tej samej nazwie…

Tak utworzone moduły importuje się zwykle później w modułach odpowiadających im „reducerów”. Dzięki temu możesz skorzystać ze zdefiniowanych tutaj stałych właściwości type. Poza tym, importuje się je w miejscu definicji funkcji mapDispatchToProps – wspominałem o tym w poprzednim wpisie. Za chwilę będę omawiać usprawnienia Redux dotyczące funkcji mapujących więc nie ma sensu dalej rozwijać tutaj tego tematu.

Łączenie w jedną tablicę

Zanim przejdziemy do upraszczania funkcji mapujących, warto sobie wszystkie funkcje kreatorów akcji zgrupować w tablicy (przyda nam się to za chwilę):

Uproszczenie funkcji mapujących

Jak już przed momentem wspomniałem, opisywane dziś przeze mnie usprawnienia Redux obejmują też uproszczenie funkcji mapujących mapStateToProps oraz mapDispatchToProps. Najlepiej jest te funkcje wyciągnąć do osobnych modułów/plików. Spójrzmy jak może wyglądać ich implementacja…

Funkcja mapStateToProps

Tutaj sprawa jest prosta. Funkcja ta zwykle po prostu przekazuje dalej obiekt statnu:

Zwróć uwagę, że wykorzystuję tutaj operator spread. Dzięki niemu nie muszę po kolei wypisywać wszystkich właściwości obiektu state.

Dzięki takiej implementacji funkcji mapStateToProps cały stan aplikacji dostępny jest w komponentach ReactJS. Biorąc pod uwagę nasz przykład „reducera” counterReducer, byłby on dostępny w komponencie poprzez this.props.counterReducer. To wszystko dzięki funkcji combineReducers, której użyliśmy wcześniej w tym artykule.

Funkcja mapDispatchToProps

W poprzednim wpisie funkcja ta wyglądała mniej więcej tak:

Teraz jednak wprowadziliśmy do obiegu funkcje kreatorów akcji. Przenieśmy więc powyższą funkcję do osobnego pliku i stwórzmy nowy moduł korzystający z naszych kreatorów akcji:

Jak widzisz, tym razem użyłem kreatorów akcji do utworzenia obiektów akcji, które następnie przekazuję do funkcji dispatch.

Uproszczenie

Tyle, że to nadal mnóstwo kodu do napisania. Z każdym razem gdy dodajesz nową akcję, tutaj też musisz dodać nową funkcję wywołującą funkcję dispatch. Można to na szczęście uprościć:

Jak widzisz, jest tutaj kilka importów. Z pakietu redux pobieram funkcję bindActionCreators, o której za chwilę. Zwróć też uwagę, że importuję actions. Jest to stworzona przez nas wcześniej tablica grupująca wszystkie funkcje kreatorów akcji.

Przejdźmy do implementacji funkcji mapDispatchToProps. Jak widzisz, najpierw tworzona jest struktura danych Map z Immutable.js. Na jej rzecz wywoływana jest funkcja merge, która łączy wszystkie funkcje z tablicy actions i umieszcza je w obiekcie Map. Następnie filtrujemy wszystkie właściwości zapisane w obiekcie Map tak aby pozostały w nim tylko funkcje. Na koniec transformujemy obiekt Map na czysty obiekt JavaScript.

Czas teraz przejść do obiektu zwracanego przez funkcję mapDispatchToProps. Jak widzisz, zawiera on właściwość actions, której przypisywana jest wartość zwracana przez wywołanie funkcji bindActionCreators. O funkcji tej możesz więcej przeczytać w dokumentacji. Ja w skrócie napiszę, że przyjmuje ona jako parametry: obiekt zawierający wszystkie funkcje kreatorów akcji oraz funkcję dispatch. Zwraca ona natomiast obiekt, zawierający właściwości odpowiadające nazwom kreatorów akcji, do których przypisane są funkcje wywołujące funkcję dispatch.

Aby lepiej sobie zobrazować jak w rezultacie wygląda obiekt przypisany do właściwości actions przedstawiam poglądowy przykład:

Obiekt zwracany przez funkcję matDispatchToProps przypisywany jest później za pomocą funkcji connect do „propsów” dostępnych w komponentach ReactJS. Jak się więc domyślasz, poszczególne akcje dostępne są w komponencie za pomocą this.props.actions.nazwaAkcji.

Funkcja connect jako dekorator

Skoro już mówimy o funkcji connect to istnieje wygodniejszy sposób jej wywołania. Ostatnio robiliśmy to tak:

Dla przypomnienia: Counter w powyższym przykładzie jest komponentem ReactJS. Warto jednak wiedzieć, że funkcja connect może też zostać użyta jako dekorator. Obczaj poniższy przykład:

Zwróć uwagę na znak @ stojący przed connect. To właśnie mówi kompilatorowi, że ma do czynienia z dekoratorem. Generalnie dekoratory to funkcje, które biorą to co znajduje się bezpośrednio pod nimi i owijają to w funkcję dekorującą. Czyli dzieje się tutaj dokładnie to samo co w przykładzie bez dekoratora: funkcja connect wykonuje się, a jej rezultat to kolejna funkcja, która bierze komponent ReactJS jako parametr. Jest to po prostu inny zapis tego samego.

Dekoratory należą do specyfikacji ES7 i w chwili pisania tego artykułu wymagają użycia odpowiedniej wtyczki do Babela. Możesz poczytać na ten temat na przykład tutaj.

Usprawnienia Redux – podsumowanie

To wszystkie usprawnienia Redux jakie na dziś przygotowałem. Mam nadzieję, że będą one dla Ciebie przydatne. Jednocześnie zachęcam do dzielenia się spostrzeżeniami w komentarzach – jeśli coś pominąłem albo uważasz, że robię to źle, daj znać – chętnie podyskutuję.

CHCESZ DARMOWEGO E-BOOKA?

Jeśli chcesz otrzymać mojego e-booka: Rozmowa Kwalifikacyjna - pytania z podstaw JavaScript zostaw mi swój e-mail:

Oprócz tego co poniedziałek dostaniesz maila z listą moich wpisów z poprzedniego tygodnia!

  • Krzysztof Trzos

    Masz może jakiś projekt na GitHub z określoną strukturą katalogów i całym przykładem, który opisujesz w niniejszym artykule?

    • akurat do tego nie mam – wyszedłem od przykładu z poprzedniego wpisu i nie wrzuciłem na githuba niestety… może jak będę mieć chwilę to postaram się coś wrzucić ale nie obiecuję…

      • Krzysztof Trzos

        Ok, rozumiem. Głównie chodzi mi o dowiedzenie się czegoś na temat najlepszej (najlepszych praktyk / standardów) struktury katalogów oraz plików poszczególnych komponentów w aplikacjach ReactJS. Jak dokonać najbardziej optymalnego ułożenia, gdzie wrzucać plik z akcjami, gdzie z reducerami itd.
        Poszukuję takiej informacji też nie tylko dla ReactJS+Redux, ale i po prostu dla Reacta.

        • A widzisz – wpis na ten temat mam w planach… Ale nie potrafię dziś powiedzieć kiedy się pojawi, bo lista pomysłów na wpisy jest długa, a prawie codziennie ktoś do mnie pisze z prośbą o opisanie czegoś 😉

          • Krzysztof Trzos

            I see…
            Jeżeli znalazłbyś jakiś konkretny przykład na Git, czy gdzieś, to mi to by w zupełności wystarczyło, jakby co :).
            I tak szacun, że ogarniasz się z tak częstym wypuszczaniem postów. U siebie też mam dość sporą listę, ale nie ma ciagle kiedy pisać. Tu robótka, tutaj nauka nowych rzeczy, tu czytanie artykułów z wielu miejsc. Znajdź czas jeszcze na sen i napisanie tekstu :D.

          • Dźwiedziu

            W ramach nauki zacząłem robić taki projekcik, aplikacja do katalogowania książek, które się ma w domu. Tj. w jakim pokoju, w jakim meblu i na jakiej półce. Technologia: react + redux + router + rxjs + bootstrap + REST API client. Bazuje na projekcie wygenerowanym przez yeoman generator-react-webpack-redux. Backend jest w Django. Chcę by był on przykładem jednej z najlepszych praktyk struktury projektu. https://github.com/dzwiedziu-nkg/books-collection-frontend/

  • Dźwiedziu

    Skąd reducery, po rozbiciu na kilka plików, wiedzą o initial state? Czy przypadkiem nie powinno być: const userReducer = (state = initialState, action) => {?

    • Tak, dokładnie tak powinno być – jeśli mamy wiele reducerów, każdy ma swój initialState i przekazujemy go jako domyślną wartość parametru state reducera. Dzięki za zwrócenie uwagi – już poprawione!

  • Nes1k

    return state.set(‚counter’, state.get(‚counter’) + 1);
    myślę, że warto zamiast tego użyć

    return state.update(‚counter’, counter => counter + 1)

  • syty

    a czy to nie powino byc tak:
    const initialState = Immutable.Map({ name: ”, age: 0 });
    const userReducer = (state = initialState.getObject(), action)
    sam initialState zwraca wewnetrzy obiekt Immutable nie obiekt {name: ”, age: 0 }

    • nie – zauważ jak w reducerze jest ustawiany nowy stan: return state.set('name', action.name);… chodzi właśnie o to aby operować na obiekcie Immutable, a nie na czystym obiekcie

      • syty

        mam z tym problem nie wiem jak sie odwolac w propsach dostaje object immutable ale nie wiem jak dostac sie do name np i jak ustawic nowa =wartosc

        • ustawianie nowej wartosci zawsze w reducerze (czyli w komponencie „disaptch” akcji) – tak jak w przykładzie state.set(‚klucz’, nowaWartosc);
          dostęp do wartości za pomocą get(‚klucz’) – można też zamiast struktury Map użyć Record – wtedy możesz pobierać wartości przez właściwość: state.klucz (ale ustawianie zawsze za pomocą set())

          • syty

            ale w propsach jak mam komponent i constructorze chce pobrac name to this.props.state.get(„name”) ?

          • chyba w złej kolejności czytasz moje wpisy 😉 najpierw przeczytaj ten o podstawach Redux: https://nafrontendzie.pl/podstawy-redux-zarzadzanie-stanem-react/
            co do Immutable – jeśli używasz Map to tak, do wartości name dostajesz się poprzez get(), dlatego użyj Record (mój błąd, powinienem go użyć w przykładzie – zaraz poprawię)

          • syty

            var Immutable = require(‚immutable’);

            var value = Immutable.Record({ key: 1 });

            console.log(value.get(‚key’)); // 1
            value.set(‚key’, 2);
            console.log(value.get(‚key’)); // wciąż 1
            value = value.set(‚key’, 2); // należy przypisać!
            console.log(value.get(‚key’)); // 2

            moze ja jestem jakis nienormalny ale samo immutale nie dziala

          • syty

            sorki ale ten kod nie dziala, samo przyklad immutable nie dziala. Moglys wrzucic to jako gotowy kod na githuba

          • co Ci dokładnie nie działa? z tego co rozumiem to za bardzo nie rozumiesz zasady działania Immutable 😉 jak robisz state.set(‚klucz’, wartość) to nowa wartosc state jest zwracana i trzeba ja przypisac na nowo ponieważ stara wartosc jest, jak sama nazwa wskazuje, immutable czyli niezmienna
            dlatego tez w świetnie działa to z Reduxem, w którym zakłada się, że state jest niezmienny i każda zmiana stanu powoduje zwrócenie zupełnie nowej wersji CAŁEGO stanu
            za bardzo nie mam czasu teraz niczego wrzucać na githua, może w weekend ale nie obiecuję 😉

          • syty

            //import Immutable from ‚immutable’;

            const Immutable = require(‚immutable’);

            const initialState = Immutable.Record({counter: 0});

            const reducer = (state = initialState, action) => {

            switch (action.type) {

            case ‚INCREMENT’:

            return state.set(‚counter’, ‚2’);

            case ‚DECREMENT’:

            return state.set(‚counter’, ‚2’);

            default:

            return state;

            }

            };

            const store = createStore(reducer, {});

            class Root extends Component {

            render() {

            const { onIncrement, counter } = this.props;

            return (

            {counter}

            );

            }

            }

            const mapStateToProps = (state) => {

            return { …state };

            };

            const mapDispatchToProps = (dispatch) => {

            return {

            onIncrement: () => dispatch({ type: ‚INCREMENT’ })

            }

            };

            Root = connect(mapStateToProps, mapDispatchToProps)(Root);

            class App extends Component {

            render(){

            return(

            )

            }

            };

            export default App;

            tak to wyglada u mnie nie mam pocztakowego countera czyli zero po clicnieciu uttona mam error e.set not function

          • z tego co widzę, to jednak kod jest wrzucony na github: https://github.com/burczu/react-redux-improvements-example

          • syty

            wszystko stalo sie jasne przyklad na githubie troche sie rozni… mam tylko jeden problem jak zainicjuje state w reducerze:

            const InitialState = Record({ counter: 0 });
            const initialState = new InitialState;
            const reducer = (state = initialState, action) => {

            to w komponencie

            render() {

            const { counter, onDecrement, onIncrement } = this.props;

            return (

            {counter}

            +

            );}

            moj counter rowna sie nan, natomiast gdy zainicjuje w storze
            const store = createStore(reducer, initialState);

            wszystko dziala bez zarzutu

            moze masz cos w glowie na szybko gdzie robie blad jak generalnie zainicjowac stan w reducerze

          • syty

            moj blad zwracam honor w combineReducer mialem jeden reducer zamiast minimalnie dwoch myslalem ze pociagnie to, wszystko okey dziekuje za odpowiedzi i wpis

  • Mateusz Rutkiewicz

    Z dodatkowych usprawnień polecam jeszcze:

    1. redux-multi, który umożliwia dispatch wielu akcji przekazanych w tablicy:


    dispatch([
    increase(),
    decrease(),
    ]);

    2. redux-batched-subscribe, który pozwala zmniejszyć ilość notyfikacji wysłanych do listenerów stanu.

    Obie biblioteki okazały się bardzo pomocne przy dużych aplikacjach reduxowych.

Google Analytics Alternative