Redux

vzor/architektúra Flux, knižnica Redux

Záznam z prednášky

Introduction

  • (slide) je to neuveriteľné, ale stále pracujeme na baterke

    • baterka svieti
    • Dokonca vieme aj v prípade, že zariadenie baterkou nedisponuje, zobraziť správu a aplikáciu vypnúť. Dokonca to robíme už tesne po spustení aplikácie.
    • Pred prístupom k baterke vieme dokonca overiť aj to, či máme povolený prístup ku kamere na OS Android.
    • Máme ju dokonca pripravenú pre globálny trh, pretože sme vyriešili aj problematiku lokalizácie aplikácie do cudzieho jazyka.
    • Roztrhli sme ju na viac komponentov, takže miesto jedného, máme tri.
  • Dnes sa pozrieme na to, ako uchovávať stav aplikácie pomocou knižnice Redux.

Redux

  • (slide) Redux je knižnica, ktorá slúži ako stavový kontajner pre vašu aplikáciu.

    Redux Logo
  • Použitie knižnice Redux má veľký význam vo veľkých aplikáciách alebo v Single Page Application-s (SPA), kedy riadenie stavu môže byť v priebehu času zložité.

Redux and Flux

  • (slide) Je veľmi podobná Flux-u a má s ním veľa spoločného. O Flux-e môžeme povedať, že je to vzor, takže Redux je “Flux-like”, pretože je na tomto vzore založený. Skôr, ako sa teda budeme venovať Redux-u, tak si predstavíme architektúru, resp. vzor Flux.

    Flux Logo

Flux Architecture and its Main Characteristics

  • (slide) Začnime teda predstavením architektúry Flux-u. Hlavným prvkom tohto vzoru (architektúry) je dispečer. Pracuje ako istý hub pre všetky udalosti, ktoré môžu v systéme nastať. Jeho hlavnou úlohou je prijímať notifikácie o vzniknutých udalostiach, ktoré nazývame akcie a poslať ich následne do všetkých skladov.

    Flux Architecture [@y30]
  • Sklad sa rozhodne, či ho prijatá akcia zaujíma alebo nie. V prípade záujmu sklad zmení svoj vnútorný stav (uložené údaje). Táto zmena sa následne stáva spúšťačom pre prekreslenie dotknutých pohľadov, ktoré sú v našom prípade komponenty React-u.

  • Akcie môžu prichádzať do dispečera z pohľadov (React komponentov), ale rovnako tak z ľubovoľných iných komponentov systému. Napr. modul zodpovedný za spracovanie HTTP požiadavky vyšle akciu, keď takúto požiadavku úspešne spracuje.

Redux Architecture

  • (slide) Teraz sa pozrime na to, ako vyzerá architektúra Redux-u. Ako bolo uvedené, Redux je založený na Flux-e, takže architektúra bude veľmi podobná.

    Redux Architecture [@y30]
  • Komponenty aplikácie (blok React) vysielajú (dispatch) akciu. Tá so sebou nesie info o tom, čo sa stalo, ako napr.: bolo stlačené tlačidlo, vypršal časovač, vypadla sieť a pod. Tú istú akciu môžu samozrejme vyslať aj iné časti systému. Akcia sa odosiela priamo do skladu, de sa rozhodne, čo sa má na základe vzniknutej akcie vykonať. To je uvedené v reducer-och. Reducer je “pure” funkcia, pomocou ktorej je možné zmeniť aktuálny stav aplikácie. Takže akonáhle sklad prijme akciu, požiada reducer-ov, aby na základe aktuálneho stavu a prijatej akcie vrátili nový stav. Následne je aktuálny stav doručený všetkým komponentom, ktoré oň majú záujem.

Differences Redux vs Flux

  • (slide) Tým hlavným rozdielom je to, že Redux používa len jeden zdroj pravdy (z angl. single source of truth) - tzv. sklad (z angl. store). V sklade sú uložené všetky údaje, ktoré vytvárajú stav vašej aplikácie. Jednotlivé komponenty v prípade zmeny stavu túto informáciu pošlú do skladu (operácia s názvom dispatch) miesto toho, aby ju poslali zvlášť samostatným komponentom. Komponenty, ktoré majú záujem o dané údaje, sa prihlásia na ich odber zo skladu (operácia s názvom subscribe).

    Redux Store [@y31]
  • (slide) Sklad je v prípade Redux-u akýsi prostredník pre každú zmenu stavu v aplikácii. A vďaka Redux-u komponenty nekomunikujú medzi sebou priamo, ale cez sklad. Tým sa je možné vyhnúť aj prípadnému vzniku chýb, ktoré so sebou môžu niesť iné stratégie. V prípade Flux-u bol týmto prostredníkom dispečer.

    Error Prone and Confusing Strategies vs Redux [@y31]

Redux and Torch

Bootstrap

  • Ako bolo uvedené, význam Redux-u je značný v prípade veľkých aplikácií. V našom prípade jeho použitie až taký význam nemá, takže v tomto prípade to bude len ukážka jeho použitia.

  • Ak však budete pátrať po best practices, ako používať Redux v aplikáciách React Native, resp. obecne ako najlepšie používať Redux, dôjdete aj ku téme štruktúra projektu. V našom prípade nemá táto téma veľké opodstatnenie, ale pre oddelenie vecí, ktoré súvisia s Redux-om od zvyšku aplikácie vytvoríme samostatný priečinok s názvom redux/. Do neho uložíme všetky súbory, ktoré budú s Redux-om súvisieť.

  • Poďme teda po troche aktualizovať baterku o Redux a pozrieme sa na jeho jednotlivé stavebné bloky zblízka. Najprv preto:

    • odstránime každú zmienku o stave z aktuálnej aplikácie
    • odstránime vlastnosti z komponentov
  • Okrem toho vytvoríme súbor helper.js, do ktorého presunieme funkciu toggleCamera() z komponentu App. Aby však všetko opäť fungovalo, tak potrebujeme nahradiť volanie prekladovej funkcie t, ktorú sme v komponentoch získali pomocou hook-u useTranslation(), za volanie i18n.t() nad objektom i18n, ktorý importneme z lokálneho modulu i18n.js:

    import i18n from "./i18n";

Installation

  • Ešte predtým, ako začneme, nainštalujeme samotný Redux. Je to otázka dvoch balíkov: redux a react-redux:

    $ npm install redux react-redux

Creating the Store with Reducer

  • Začneme tým, že vytvoríme sklad, v ktorom sa bude nachádzať stav aplikácie. Keďže k nemu bude môcť pristúpiť ľubovoľná časť aplikácie, je dobrou praxou ho osamostatniť do osobitného súboru s názvom store.js, ktorý sa bude nachádzať v priečinku src/redux/.

  • Stav bude aj naďalej reprezentovaný stavovou premennou isOn ako doteraz a po spustení bude mať nastavenú hodnotu false.

  • Aby sme sklad mohli vytvoriť, potrebujeme vytvoriť aspoň jednu reducer funkciu.

  • Ako sme už hovorili, reducer je “pure” funkcia, takže na základe vstupu vždy vráti rovnaký výstup bez akýchkoľvek bočných následkov. To tiež znamená, že reducer by nemal volať asynchrónne funkcie.

  • Reducer dostane dva argumenty funkcie:

    • aktuálny stav, a
    • akciu, ktorá bola vyvolaná.
  • Reducer vždy vráti nový stav aplikácie.

  • V našom prípade si vystačíme s jedným reducer-om, ktorý uložíme do súboru so skladom. Pre náš prípad bude teda vyzerať takto:

    // in redux/store.js
    function torchReducer(state = {isOn: false}, action){
        return state;
    }
  • Zadefinovali sme akurát podobu počiatočného stavu, kde bude mať premenná isOn hodnotu false.

  • Teraz môžeme vytvoriť nový sklad:

    // in redux/store.js
    import { createStore } from "redux";
    
    export const store = createStore(torchReducer);

Subscribing to State Changes

  • Sklad je síce vytvorený, ale jednotlivé komponenty zatiaľ nemajú prístup k jeho obsahu. Je teda potrebné zabezpečiť distribúciu jednotlivých zmien pre jednotlivé komponenty.

  • Za týmto účelom je potrebné zabaliť hlavný komponent aplikácie do samostatného komponentu s názvom Provider, ktorý dostane ako parameter objekt skladu:

    // App.js
    import { Provider } from "react-redux";
    import { createStore } from "redux";
    import { store } from '../redux/store';
    
    export function App() {
        return (
            <Provider store={store}>
                <View style={styles.container}>
                    <ImageSwitch></ImageSwitch>
                    <ButtonSwitch></ButtonSwitch>
                </View>
            </Provider>
        );
    }
  • Komponent Provider nemá vizuálnu podobu. Zabezpečí však distribúciu zmien všetkým dotknutým komponentom (svojim potomkom), ktorí sa nachádzajú vo vnútri tohto komponentu.

  • (slide) Chýba nám však ešte prihlásenie sa na odoberanie akcií (udalostí) súvisiacich s aktualizovaním stavu isOn. Na prihlásenie sa komponentu k odberu akcie použijeme hook useSelector(). V prípade komponentu ButtonSwitch bude jeho použitie vyzerať takto:

    import { useSelector } from "react-redux";
    
    export function ButtonSwitch() {
        const { t } = useTranslation();
        const isOn = useSelector(function(state){
          return state.isOn;
        });
    
        return (
            <Button
                onPress={null}
                title={isOn === true ? 
                    t("Turn Off") : t("Turn On")}
            />
        );
    }
  • Podobne aktualizujeme aj komponent ImageSwitch:

    import { useSelector } from "react-redux";
    
    export function ImageSwitch() {
        const isOn = useSelector(function(state){
           return state.isOn;
        });
    
        const image = isOn
            ? require("../assets/bulb_on.png")
            : require("../assets/bulb_off.png");
    
        return (
            <Pressable onPress={null}>
                <Image source={image} />
            </Pressable>
        );
    }
  • Ak teraz spustíme aplikáciu, bude reagovať na zmenu stavu. Tú teraz vieme zmeniť iba natvrdo v počiatočnom stave z hodnoty false na true a opačne. Je však možné vidieť, že aplikácia sa naozaj zmení na základe hodnoty tohto stavu.

  • Ako je možné vidieť, aktuálnu hodnotu stavu nemáme od predka získanú pomocou vlastností (properties), ale priamo zo skladu pomocou hook-u useSelector().

Actions

  • Takže aktualizácia aplikácie na základe stavu nám už funguje. Teraz sa poďme pozrieť na to, ako vytvoriť akciu (udalosť) vedúcu k zmene stavu.

  • (slide) Obecne je možné akciu charakterizovať ako udalosť, ktorá nastala. Je to teda objekt opisujúci vzniknutú udalosť.

  • Akcia v sebe nesie dva typy informácií:

    • typ akcie, a
    • metaúdaje opisujúce alebo nesúce detaily akcie (vzniknutej udalosti).
  • Ak sa pozrieme na bežné udalosti, nájdeme jasné paralely. Napr. ak za udalosť pokladáme pohyb myšou (typ udalosti), tak jej metaúdaje budú napr. aktuálna poloha myši ([x, y]) a možno aj stav tlačítiek. Ak bol stlačený kláves (typ udalosti), tak metaúdaj bude určite obsahovať kód klávesy, resp. kláves, ktorý/é boli stlačené.

  • V našom prípade môžeme obecne rozmýšľať o týchto typoch akcií:

    • BUTTON_PRESSED - keď dôjde k stlačeniu tlačidla,
    • IMAGE_PRESSED - keď dôjde k stlačeniu obrázka, a
    • STATE_CHANGED - keď reálne dôjde k zmene stavu (k zmene dôjsť nemusí aj po stlačení, ak aplikácia nemá povolený prístup ku kamere)
  • Ako som už spomínal, akcia je objekt. Jediný kľúč, ktorý je povinný, je typ akcie type.

  • Akcia samotná nemusí so sebou niesť údaje, ale samozrejme môže. To je vlastne aj náš prípad, nakoľko akcie BUTTON_PRESSED a IMAGE_PRESSED údaje niesť nepotrebujú, ale akcia STATE_CHANGED bude niesť so sebou nový stav.

  • Typy akcií osamostatníme do osobitného súboru s názvom redux/actions.js. V ňom vytvoríme samotné konštanty reprezentujúce jednotlivé typy. Tento prístup so sebou nesie niekoľko výhod:

    • nedôjde k zbytočnému preklepu, ak je typ akcie reprezentovaný reťazcom, čo môže mať za následok znefunkčnenie aplikácie,
    • ochrana pred preklepmi, k čomu nám samozrejme pomôže aj vývojové prostredie, ktoré bude názvom premenných rozumieť,
    • možnosť zdokumentovať všetky typy akcií, ktoré môžu byť v aplikácii vytvorené, na jednom mieste,
    • podpora v nástrojoch pre vývojárov.
  • Súbor actions.js sa bude nachádzať v priečinku src/ a bude vyzerať nasledovne:

    export const BUTTON_PRESSED = 'BUTTON_PRESSED';
    export const IMAGE_PRESSED = 'IMAGE_PRESSED';
    export const STATE_CHANGED = 'STATE_CHANGED';
  • Akcie zatiaľ vytvoríme ako vo forme objektov, ktoré vložíme do príslušných komponentov, ktoré ich budú odosielať (dispatch):

    • akciu BUTTON_PRESSED vložíme do komponentu ButtonSwitch:

      import { BUTTON_PRESSED } from "../redux/actions";
      
      const action = {
          type: BUTTON_PRESSED
      }
    • akciu IMAGE_PRESSED vložíme do komponentu ImageSwitch:

      import { IMAGE_PRESSED } from "../redux/actions";
      
      const action = {
          type: IMAGE_PRESSED
      }
    • akciu STATE_CHANGED budeme posielať zvnútra funkcie toggleCamera(), takže si ju zatiaľ pripravíme v súbore heleper.js:

      import { STATE_CHANGED } from "../redux/actions";
      
      const action = {
          type: STATE_CHANGED,
          isOn: null  // new state goes here
      }

Dispatching the Action

  • (slide) Na odoslanie akcie z komponentov priamo do skladu potrebujeme poznať vhodnú funkciu. Od verzie 7.1 Redux obsahuje na tento účel hook s názvom useDispatch(), ktorý použijeme aj my. Tento hook vráti funkciu dispatch(), ktorú použijeme na komunikáciu s vytvoreným skladom:

    import { useDispatch } from "react-redux";
    import { BUTTON_PRESSED } from "../redux/actions";
    
    export function ButtonSwitch() {
        const dispatch = useDispatch();
        const action = {
          type: BUTTON_PRESSED
      }
    
        return (
            <Button
                onPress={function() {
                  dispatch(action);
              }}
                title="Turn On"
            />
        );
    }

Handling the Action with Reducer

  • Po stlačení tlačidla bude doručená akcia typu BUTTON_PRESSED do skladu, kde sa stane argumentom reducer funkcií, v našom prípade funkcie torchReducer(). Tá je aktuálne prázdna, takže ju aktualizujeme tak, aby vedela zareagovať na každý typ akcie jeho zalogovaním:

    function torchReducer(state = { isOn: false }, action) {
        console.log(">> reducer invoked");
    
        switch (action.type) {
            case BUTTON_PRESSED:
                console.log(">> button pressed");
                return state;
    
            case IMAGE_PRESSED:
                console.log(">> image pressed");
                return state;
    
            case STATE_CHANGED:
                console.log(">> state changed");
                return state;
    
            default:
                console.warn(
                    `Action type "${action.type}" was not handled.`
                );
                return state;
        }
    }

Dispatching and Handling the Action STATE_CHANGED

  • Ako už bolo uvedené skôr, reducer je “pure” funkcia. Z toho vyplýva, že ošetrenie akcie by malo byť veľmi rýchle a bez bočných efektov alebo volania asynchrónnych funkcií. Pre nás to znamená, že v reducer funkcii nebudeme volať objekt baterky, aby sa rozsvietil, prípadne zhasol. To totiž so sebou nesie niekoľko bočných funkcionalít, ako je asynchrónne volanie alebo overovanie prístupu ku kamere na platforme Android.

  • Preto po stlačení tlačidla a vytvorení akcie typu BUTTON_PRESSED zavoláme funkciu toggleTorch(). Dôoležité však bude upraviť jej kód, kedy v prípade, že sa baterka má naozaj rozsvietiť, vyšle funkcia akciu STATE_CHANGED s hodnotou nového stavu v premennej isOn.

  • Najprv teda zavoláme funkciu toggleTorch() po stlačení tlačidla:

    // components/ButtonSwitch.js
    import { toggleTorch } from "../helper";
    
    return (
        <Button
            onPress={function () {
                dispatch(action);
                toggleCamera();
            }}
            title={isOn === true ? t("Turn Off") : t("Turn On")}
        />
    );
  • Následne upravíme kód funkcie toggleTorch() tak, aby pri úspešnom zmene stavu baterky bola táto informácia distribuovaná do skladu:

    // helper.js
    if (cameraAllowed) {
        const state = store.getState();
        await Torch.switchState(!state.isOn);
        store.dispatch({
            type: STATE_CHANGED,
            isOn: !state.isOn,
        });
    }
  • Nakoniec už len upravíme príslušnú vetvu v reducer funkcii, kde vytvoríme nový stav na základe hodnoty získanej z akcie:

    
    case STATE_CHANGED:
        console.log(">> state changed");
        return {
            ...state,
            isOn: action.isOn,
        };
  • Ak teraz aplikáciu vyskúšame, bude fungovať tak, ako má - po stlačení tlačidla dôjde nie len k rozsvieteniu blesku na zariadení, ale aj k zmene stavu komponentov na základe aktuálneho stavu v sklade. Jediné, čo zostáva aktualizovať, je ošetrenie stlačenia obrázka v komponente ImageSwitch, ktoré bude vyzerať podobne, ako v prípade komponentu ButtonSwitch:

    // components/ImageSwitch.js
    import { toggleTorch } from "../helper";
    
    return (
        <Button
            onPress={function () {
                dispatch(action);
                toggleTorch();
            }}
            title={isOn === true ? t("Turn Off") : t("Turn On")}
        />
    );

Conclusion

  • Dnes sme si predstavili knižnicu Redux, pomocou ktorej sme uchovali stav našej aplikácie. Jej použitie na malých projektoch nemá veľký význam, ale oceníte ju najmä vtedy, ak vytvárate veľké a komplexné aplikácie.

  • Nabudúce nadviažeme na túto tému a pozrieme sa na ďalšie možnosti perzistencie údajov vo vašich aplikáciách.