Ve svém předchozím článku jsem rozebíral, jak používat Immutable.js k správě reduxového stavu. V podstatě jsem vám dal na výběr: Nepoužívejte Immutable.js a riskujte špatně odhalitelné chyby a nebo jej používejte a připravte se o pohodlí destrukturalizace objektů a přehlednost kódu. Mohli jste se ptát: Proč ne obojí? A já bych řekl: Nejde to, alespoň ne jednoduše. A neměl bych pravdu.

Když jsme začali používat Immutable.js, navyklí na netypový svět JavaScriptu, zvykli jsme si používat Map pro ukládání objektů. A úplně jsme při tom opomněli Record, který by byl v mnohém vhodnější. Jaký je mezi nimi rozdíl?

Map může mít libovolné klíče, zatímco pro Record je nutné je definovat na začátku. Z perspektivy silně typovaného jazyka je Map asociativní pole (slovník), zatímco Record je klasický objekt. Pro ty, kdo jsou zvyklí na objektově orientované jazyky (a u nás většina lidí umí dobře Javu), je to naprosto přirozené. Například nadefinuju třídu Position:


const Position = Record({x: null, y: null});

Všechny objekty typu Position pak mají právě dva atributy: x a y. Můžu k nim přistupovat jako k immutable Map, zároveň však mají JavaScriptové gettery:


const pos = Position({x: 10, y: 15});

pos; // Record{x: 10, y: 15}
pos.get('x'); // 10
pos.x; // 10
pos.set('x', 15); // Record{x: 15, y: 15})

const {x, y} = pos;
x + y; // 25

Funguje tedy destrukturalizace objektů! Jdou ale i zajímavější kousky, například definovat si vlastní metody:


class Rectangle extends Record({a: 0, b:0}) {
getArea() {
return this.a * this.b;
}
getPerimeter() {
return this.a + this.b;
}
isSquare() {
return this.a === this.b;
}
}

Je libo Record, který pro každý atribut definuje getter a setter ve stylu Javy?


import {Record} from 'immutable';

export default (defaults) => {
const javaRecord = Record(defaults);
Object.keys(defaults).forEach((property) => {
const propertySuffix = property.slice(0, 1).toUpperCase() + property.slice(1);
javaRecord.prototype[`get${propertySuffix}`] = function() {
return this.get(property);
}
javaRecord.prototype[`set${propertySuffix}`] = function(newValue) {
return this.set(property, newValue);
}
});
return javaRecord;
}

Co je ale na Recordu nejlepší? Kromě toho, že má přesně definovaný výčet klíčů, chová se Record jako immutable Map, fungují tedy na něm všechny hluboké změny, jako setIn nebo mergeIn.

Použití pro stav

V minulém článku jsem naznačil, že jsme měli problém se složitějšímy propsy. Konkrétně šlo o tabulku, kdy seznam sloupců byl statický (tedy plain JS objekt), zatímco data jsme brali ze stavu a byla immutable. Přitom ke každému z nich se přistupuje různě, k obyčejným objektům přes tečky, k immutable pomocí metody get. Abychom se vyhli této dichotomii a nutnosti převádění, rozhodli jsme se, že všechny strukturované propsy budou immutable.

immutability_ancient_aliens

Jak v takovém případě použijeme Record? Sloupečky nadefinujeme jako seznam obyčejných JS objektů. Data pak budou List řádků, kde každý řádek je Record. Uvnitř komponenty Table můžeme všude používat na přístup k hodnotám tečkovou notaci. Pokud bychom měli sloupečky uložené ve stavu, můžou být také List Recordů a nic se nezmění. Co je výborné, můžeme nadefinovat propTypes pomocí shape a bude je splňovat jak Record, tak obyčejný objekt.

Samozřejmě, takový přístup není bez nevýhod. Zaprvé, sice funguje destrukturalizace, ale zato nefunguje operátor spread, který tím pádem nesmíme používat:


const Test = ({x, y}) =>
<div>{x}:{y}</div>
;
const test = ({x, y}) => x + y;

const coord = new (Record({x: 7, y: 8}));

//
<div>:</div>
{Test(coord)} //
<div>7:8</div>
{test(coord)} // 15

Druhým problémem je, že musíme explicitně vypsat všechny atributy, které může Record mít, což je mírně nepohodlné, když data přichází ze serveru a může jich být hodně. A hlavně, musíme jejich seznam znát předem. Skutečně se tím blížíme silně typovaným jazykům, kde musíme definovat DTO. K tomu se váže další drobnost, a to, že Record nijak nekontroluje, že dostal hodnoty pro všechny atributy. Pokud tedy nějaká hodnota ze serveru nepřijde, vezme defaultní, což nemusí být žádané. Můžeme ale nadefinovat následující třídu, která akceptuje seznam atributů a v konstruktoru kontroluje, že jsou všechny definované:


import {Record} from 'immutable';

export default (...properties) => {
const defaults = properties.reduce((result, prop) => Object.assign({[prop]: null}, result), {});
return class NonEmptyRecord extends Record(defaults) {
constructor(values) {
super(values);
properties.forEach(prop => {
if (values[prop] === undefined) {
throw new Error(`${prop} is undefined`);
}
});
}
};
}

Co když nám může přijít opravdu libovolný objekt, Record tedy nestačí a potřebujeme Map? Pak jsme asi opravdu nahraní. Můžeme se jen zamyslet, jestli nemůžeme komponentu rozdělit a napojit ji na redux store. Nevěřím však, že se s tímto problémem budeme setkávat často.

Závěrem

Pokud immutable Map je asociativní pole s libovolnými klíči, Record je neměnný objekt tak jak ho známe z objektového programování. K jeho atributům můžeme přistupovat tečkovou notací a funguje tedy destrukturalizace objektů. Jako objektu mu můžeme definovat vlastní metody. Zároveň na něm fungují hluboké modifikace, které známe z Immutable Map.

Pokud používáme místo Map ve stavu Record, můžou všechny komponenty používat tečkovou notaci a React.PropTypes.shape. Jediné, čemu se musíme vyhnout je operátor spread. Musíme ale objekty, které používáme, nadefinovat, tedy znát jejich strukturu, jejich atributy. V případě DTO používaných na komunikaci se serverem to například není problém.

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?

Mrkni 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.