Pokud to s Reduxem myslíte vážně, a já doufám, že ano, budete muset dříve nebo později řešit problém, jak dosáhnout neměnnosti (immutability) stavu. A věřte mi, chcete jej řešit spíše dříve, než později, abyste se vyhnuli náročnému refactoringu, který půjde přes všechny komponenty vaší aplikace.
O co tedy jde? Stav aplikace Redux drží ve store jako jeden objekt a modifikuje jej pomocí reducerů. Reducer je funkce bez vedlejších účinků, která vezme stav a akci a vrátí nový stav. Opakuji – reducery nemají vedlejší účinky, tedy především nemodifikují své argumenty (a zdůrazňuje to i dokumentace reduxu). Stav tedy v reduceru musíte kopírovat – můžete ale sdílet ty části, které se nemění. Redux s tím počítá a pokud zdrojový stav – argument – nějak změníte, byť omylem, dočkáte se nepěkných chyb.
Ideálně by tedy stav měl být neměnný. Redux bohužel nic takového nevynucuje, musíte si to ohlídat sami. Pokud si myslíte, že jste dost dobří, můžete věřit, že kód píšete správně, ale dříve nebo později někde uděláte chybu. Jaké jsou tedy možnosti?
Klonování stavu
Celý stav můžete naklonovat (například přes JSON.parse(JSON.stringify(state))) a pak jej měnit podle libosti. Pro malé testovací projekty to může být dobré řešení, dost zásadně to ale aplikaci zpomalí:
- Pokaždé kopírujete celý stav, a to i ty části, které se nezměnily. Pokud je stav malý, nemuselo by to vadit.
- Tím že pokaždé vytváříte nový stav, musí se pokaždé překreslit všechny komponenty, alespoň do fáze virtuálního DOMu (a pak porovnat s minulým virtuálním DOMem). Redux totiž obsahuje optimalizaci, která zařídí, že pokud se nezmění stav, na základě kterého se komponenta generuje, nepřekreslí se ani virtuální DOM (dosahuje toho pomocí shouldComponentUdpate).
Kvůli výkonu tedy nemá velkou cenu se o něco podobného pokoušet. Mohlo by to mít smysl u pokusných projektů.
React-addons-update
Rozhodně nechcete kopírovat ty části stavu, které se nezměnily. Abyste to měli jednodušší u zanořených objektů, zveřejnil Facebook jednoduchou pomocnou funkci, které formou příkazu zadáte, co kde chcete změnit a ona vám vrátí kopii stavu s danou změnou. Doporučuju si přečíst zdrojový kód (odkaz na podobnou knihovnu), v zásadě je velmi jednoduchá.
Výhodou této funkce je, že v novém objektu znova použije ty části stavu, které se neměnily, fungují tedy optimalizace zabudované v Reduxu. Drobnou nevýhodou je, že pokud omylem stav změníte, neklepne vás přes prsty. To se vám může stát, když taháte věci ze starého stavu (napíšete splice místo slice a problém je na světě). I to jde vyřešit, například pomocí Object.freeze(), ale nejsem si jistý, jestli je to zrovna nejefektivnější řešení.
Immutable.js
Immutable.js je knihovna, která řeší všechny tyto problémy. Brání nechtěným změnám objektu, poskytuje funkce, které umožňují jednoduše vytvořit nový objekt s danými změnami a nekopíruje ty věci, které se nezměnily. Navíc by měla být rychlejší než jednoduché funkce typu react-addons-update.
Samozřejmě; musíte si zvyknout, že už nepracujete s obyčejnými JS objekty („strukturami“), ale s objekty ve smyslu objektového programování. Můj asi největší problém s Immutable.js je, že Map, používaný místo objektu, je dost … ukecaný. Při získávání hodnot je místo tečkové notace, tedy object.property třeba psát object.get(‚property‘). To vypadá jako drobnost, ale není to úplně pohodlné. Z toho důvodu taky nefunguje destrukturalizace objektů (destrukturalizace Listů funguje normálně). Autoři o tomto problému ví, bohužel, není technicky jednoduché jej řešit (viz diskuse).
Rovnost referencí
Javascript neumožňuje porovnávat objekty do hloubky: Operátor === vždy porovnává reference — adresy v paměti — tedy platí, že:
({a: 1} === {a: 1}); // false
Objekt na levé i pravé straně se vytvoří zvlášť, jsou tedy na jiném místě v paměti. Můžete si napsat vlastní funkci, která objekty porovná do hloubky, ale nebude příliš rychlá. Když Redux zjišťuje, které komponenty má znova vykreslit, porovnává změny skrz pomocí ===.
Knihovna Immutable.js je výborná v tom, že při změně stavu ty jeho části, které není třeba změnit, zachovává. Platí pro ně tedy rovnost referencí. Neznamená to ale, že jde používat pro hluboké porovnávání objektů – funguje jen v tom případě, že porovnáváme části objektu před změnou a po změně. Pro názornost:
Map({a: 1}) === Map({a: 1}); // false const before = fromJS({a: 1, b: {c: 2}}); const after1 = before.set('a', 3); const after2 = before.set('a', 3); after1 === after2; // false after1.get('b') === after2.get('b'); // true
Zajímavá je funkce mergeDeep, která rozhodne, jestli je změna vůbec potřeba:
const before = fromJS({a: 1, b: {c: 2}}); const after = before.mergeDeep({b: {c: 2}}); before === after; // true
Immutable.js a Reactí komponenty
Když jsme na našem projektu začali pracovat s Immutable.js, bylo to dost živelné. Výsledkem bylo, že nikdo nevěděl, kde se Immutable objekty používají a kde ne. Někde je to jasné: ve stavu a reducerech se pracuje s Immutable objekty. V akcích se vždy posílají „ty obyčejné“. Problém však nastal u UI komponent. Konkrétně šlo o znovupoužitelné komponenty typu tabulka.
Ideálně píšeme komponenty tak, že jejich parametry jsou primitivní hodnoty – nadpis, jméno uživatel, … Některé komponenty jsou ale složitější a vyžadují složitější parametry. Například tabulka bere pole sloupců a pole řádků. A jak řádek tak sloupec samy o sobě primitivní nejsou. Přitom řádky bereme ze stavu, jsou tedy Immutable, zatímco sloupce jsou typicky statické, a tedy obyčejné objekty. A teď, když píšete tabulku nebo v ní děláte změnu, pamatujte si, co je Immutable a co ne. Přitom se s nimi zachází různě (tečková notace a metoda get).
Doporučuji se už na začátku rozhodnout pro některý z přístupů, jinak vás čeká náročný refactor (já jsem jím strávil celý týden). Zde je několik možností, nad kterými jsme uvažovali. Možná přijdete na další:
- Používat všude obyčejné objekty. To je možné jen v případě, že váš stav není Immutable – čistě hypoteticky můžete na stav před tím, než ho použijete v komponentě, zavolat toJS() a použít jako obyčejný objekt, ale připravíte se tím o optimalizaci v Reduxu. Navíc toJS() je poměrně náročná operace.
- Používat všude Immutable. Tedy pokud předáváte komponentám nějaký statický objekt (seznam sloupců), musíte ho převést do Immutable. To je například cesta, kterou jsme se vydali my.
- Používat Immutable pro věci ze stavu a obyčejné objekty pro statické. To rozhodně nedoporuči, protože se může stát, že některé parametry se občas budou brát ze stavu a občas budou statické. Každopádně to znamená, že programátoři se budou muset více soustředit na to, jestli předávají správný typ (a PropTypes vás nezachrání).
- Používat Immutable a statické věci předávat jako komponenty v Reactu. To by v případě tabulky znamenalo asi následující konstrukci, ale takový přístup pravděpodobně není možné nebo vhodné použít všude (ale používá se – viz React Bootstrap Table), uvádím jej tady proto, že pro některé komponenty (například menu) to může být zajímavá alternativa.
<table rows={/* řádky ze stavu */}> <column label="Jméno"></column> <column label="Příjmení"></column> </table>
Rozhodně platí, že co nejvíce komponent by mělo být napojených na stav a ideálně se budovat z primitivních typů. Otázkou je, jestli je to možné vždy – kromě toho že občas budeme potřebovat pole/seznamy primitivních hodnot. Možná by ale šlo například tabulce předat řádkový selektor s tím, že každý řádek by byl napojený na stav a použil tento selektor na získání svého obsahu.
Závěrem
V Reduxu budete dříve nebo později muset neměnnost stavu nějakým způsobem řešit a Immutable.js je pravděpodobně nejlepší řešení, hlavně z hlediska rychlosti a pohodlí programátorů (i přes drobné nedostatky). Je třeba ale rozhodnout, kde a jak se bude používat, především co se týká React komponent, aby byl kód konzistentní.
Tomáš Vejpustek
Tomáš je Java a JavaScript developer se zaměřením na front-end a UX. Zajímá se o využití výpočetní techniky k řešení každodenních problémů. Chce vytvářet taková uživatelská rozhraní, která by sám rád používal.
Máš rád front-end stejně jako Tom nebo ho chceš jako on ovládat?
Hledáme do naší party skvělé vývojáře – třeba právě na projekt eBay nebo React.js developra.
Mrkni taky na všechny pozice, co nabízíme a dej nám o sobě vědět, bez ohledu na to, zda máš rád front-end nebo back-end, jestli nějakou technologii umíš nebo ne. Pokud budeš ty sám chtít, dostaneš u nás příležitost naučit se, co tě zajímá nebo udělat něco výjimečného.
Napsat komentář