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.
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 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.
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.
Poznámka
Ak by sme chceli Flux porovnať s MVC, tak by sme mohli povedať, že sklad reprezentuje model, pretože sklad udržiava údaje aktuálne.
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á.
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).
(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.
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 funkciutoggleCamera()
z komponentuApp
. Aby však všetko opäť fungovalo, tak potrebujeme nahradiť volanie prekladovej funkciet
, ktorú sme v komponentoch získali pomocou hook-uuseTranslation()
, za volaniei18n.t()
nad objektomi18n
, ktorý importneme z lokálneho modului18n.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
areact-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činkusrc/redux/
.Stav bude aj naďalej reprezentovaný stavovou premennou
isOn
ako doteraz a po spustení bude mať nastavenú hodnotufalse
.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; }
Poznámka
Ak budete sledovať odporúčania v súvislosti s Redux-om a organizovaním projektu, stretnete sa s tým, že všetky reducer funkcie sú umiestnené v samostatnom súbore
reducers.js
. V našom prípade máme len jeden reducer a bude umiestnený v rovnakom súbore, ako sklad.Zadefinovali sme akurát podobu počiatočného stavu, kde bude mať premenná
isOn
hodnotufalse
.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.Poznámka
Na účel distribúcie údajov všetkým prihláseným komponentom sa používa
Context
, o ktorom sme hovorili už skôr. Je to logické -Provider
je vlastne rodičom všetkých komponentov, ktoré sa v ňom nachádzajú, čo vie využiťContext
doručením nového stavu komponentom nachádzajúcim sa v celom podstrome.(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 hookuseSelector()
. V prípade komponentuButtonSwitch
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
natrue
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, aSTATE_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
aIMAGE_PRESSED
údaje niesť nepotrebujú, ale akciaSTATE_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činkusrc/
a bude vyzerať nasledovne:export const BUTTON_PRESSED = 'BUTTON_PRESSED'; export const IMAGE_PRESSED = 'IMAGE_PRESSED'; export const STATE_CHANGED = 'STATE_CHANGED';
Poznámka
Aj v súvislosti s reprezentáciou akcií v projekte sa môžete stretnúť s rozličným prístupom. Najčastejšie sa dá stretnúť s dvoma súbormi, kde v jednom sa nachádzajú samotné typy akcií (náš prípad) a v druhom sa nachádzajú kostry akcií, resp. funkcie, ktoré objekty akcií vracajú.
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 komponentuButtonSwitch
:import { BUTTON_PRESSED } from "../redux/actions"; const action = { type: BUTTON_PRESSED }
akciu
IMAGE_PRESSED
vložíme do komponentuImageSwitch
:import { IMAGE_PRESSED } from "../redux/actions"; const action = { type: IMAGE_PRESSED }
akciu
STATE_CHANGED
budeme posielať zvnútra funkcietoggleCamera()
, takže si ju zatiaľ pripravíme v súboreheleper.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 funkciudispatch()
, 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 funkcietorchReducer()
. 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; } }
Poznámka
Je dôležité mať aj vetvu
default
pre prípady, kedy nedôjde k ošetreniu danej akcie. To môže byť želané, ale rovnako tak neželané. Je teda dobré takýto prípad aspoň zalogovať, aby sme sa vyhli prípadným problémom.
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 funkciutoggleTorch()
. Dôoležité však bude upraviť jej kód, kedy v prípade, že sa baterka má naozaj rozsvietiť, vyšle funkcia akciuSTATE_CHANGED
s hodnotou nového stavu v premennejisOn
.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); .dispatch({ storetype: 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 komponentuButtonSwitch
:// 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.