pondělí 12. června 2017

Co mi vadí na React Relay?

Facebook, jakožto technologická firma, je v současné době na vrcholu. Nejenom proto, že dal světu takové věci jako je React či GraphQL, ale také pro to, že jsou to knihovny, které sám Facebook používá. Díky tomu dostanete věc, která je odladěná pro danou činnost a nemusíte se bát, že jí Facebook přestane podporovat. To je asi jeden z největších rozdílů, kterým se vyznačuje Facebook oproti Googlu. Google se v minulosti dost často ukázal jako vrah svých nenarozených dětí. Každý, kdo se kdy setkal s technologiemi jako je Dart, Angular 1 či třeba GWT by mi jistě dal za pravdu.

Nicméně, bylo by příliš krátozraké, kdyby se člověk automaticky adaptoval na to, co Facebook vypustí. Jednou z věcí, přes kterou jsem se nebyl schopen dostat, je Relay.

Dnešní článek bude opačný. Místo, abych se snažil evangelizovat danou technologii, zkusím vysvětlit, proč není Relay zrovna ideální knihovna.

Začněme hezky od začátku.

Co je Relay?


V případě, že začnete používat React a současně GraphQL, dostanete se do fáze, kdy zjistíte, že přesně pro tento "stack" je určen Relay. Zjednodušeně se dá říct, že Relay je abstraktní vrstva nad vašimi React komponentami, která zajišťuje komunikaci s GraphQL serverem a vaší view vrstvou. Odstíní vás od takových věcí jako je samotný "fetch" na server API či nastavování storage, do kterého se budou načtená data ukládat. Dále za Vás bude řešit error handling a vaše komponenty se stanou prezentační vrstvou pro grafové objekty z API. Umí zařídit optimistické mutace, kdy sami říkáte, že server změnu udělá a vy klienta obelstíte tím, že se budete tvářit, jako kdyby k tomu již došlo.

Relay za vás zařídí většinu nudných věcí. To je skvělé. Až na to, že....

Abych byl schopen popsat, co přesně mi vadí, zkusím Relay rozdělit na dvě části, kterých se to týká. Těmi částmi je GraphQL server a React klient.

GraphQL Server


V případě, že se adaptujete na Relay, dostanete studenou sprchu hned na začátku. Zjistíte totiž, že si nemůžete své GraphQL API psát tak, jak se Vám zlíbí. Relay vám bude hodně věcí předepisovat. První z věcí, kterou musíte doplnit je to, že celé query musí být obaleno jedním hlavním query, který v podstatě říká, že se dotazuji na data přihlášeného uživatele. V tom by až takový problém nebyl. Obalíme query jedním root query a je hotovo. Bohužel tomu tak není.

Další věcí, kterou musíte splnit je identifikátor jednotlivých GraphQL objektů. Jinými slovy to znamená, že identifikátor, nebo chcete-li ID musí být unikátní v globálním stavu.

Co to vlastně znamená? 


Představte si situaci, že máte objekt uživatele, který je reprezentován z databáze. V databázi má vlastní ID. Abychom zařídili globální unikátnost, používá se k tomu následující způsob: "K ID uživatele přičtu název GraphQL objektu a vytvořím unikátní hash." V případě zpětného volání dojde k tomu, že GraphQL nejdříve "rozparsuje" daný identifikátor, abychom z něj dostali ID uživatele. Toto celé se děje z prostého důvodu. Relay má na klientovi vlastní storage, ve kterém jsou jednotlivé záznamy uloženy právě s tímto globálním identifikátorem. V případě, že někde dojde ke změne, tak jí propíše všude, kde je daný identifikátor roven.

Opět uvedu příklad.
Mám tabulku online uživatelů, kde je jméno a příjmení. V jiném panelu edituji uživatele, u kterého změním jméno. V případě, že se jedná o záznam, který je použit i v oné tabulce, provede přepis i tam. Tím Relay částečně zajišťuje aktuálnost dat.

Další věcí, kterou musíte splnit jsou kolekce dat. V případě, že používáte pole záznamů (což jistě používáte), musíte tuto kolekci implementovat tak, jak vám předepisuje Relay. Jedná se zejména o stránkování mezi daty.

Poslední věcí, kterou musíte na serveru splnit jsou mutace. Respektive argumenty mutace. Nelze si je napsat jen tak, jak se mi zlíbí. Musí být obaleny přes vlastní GraphQL objekt "input".

Dobrá zpráva je, že pro všechny potřebné změny, pokytuje Relay vlastní knihovny, které v GraphQL objektech buď implementujete, nebo své objekty touto knihovnou obalujete.

Výsledkem je, že vaše GraphQL API bude připravené pro Relay, ale za jakou cenu? Nejen to, že musíte splnit dané podmínky, pro správné fungovaní Relay, ale také nemáte moc možnost z tohoto "uhnout" vlastním směrem.

Relay v tomto případě prohrává 1:0. Je příliš invazivní a příliš me svazuje.

GraphQL Client alias Relay


Nyní se přesuneme směrem ke klientovi.

Po tom, co s potem ve tváři, splníte všechna předepsaná kritéria na straně serveru, tak musíte počítat s tím, že klient na tom není o moc lépe.
Za prvé je nutné celou vaší React aplikaci obalit Relay komponentou, která je obsahuje vlastní store a zařizuje to, aby vám správně fungovalo "injectování" GraphQL dotazů do komponent.
Výsledek je poté následující. Napíšete vlastní komponentu a obalíte jí pomocí Relay, kam zapíšete GraphQL dotaz. Relay poté zařídí, že vaše komponenta dostane výsledná data pomocí "props".

Díky tomu Relay v podstatě nahrazuje nejznámější implemenaci Fluxu a tím je Redux. Pokud byste se totiž rozhodli, že budete chtít používat Relay a k tomu Redux, musím vás zklamat. Tyto dvě technologie jdou částečně proti sobě. Máte Relay a ten vám přeci stačí.

V tom vidím největší nevýhodu na straně klienta. Nejen, že mě nutí používat Relay a zahodit Redux, ale je navíc opět dost invazivní a dost mě uzamyká někde, kde nechci být. Chci mít volnost v tom, jakým způsobem si budu vlastní komponenty navrhovat. Chci si je rozdělit na kontejnery a komponenty. V případě Relay toto neexistuje. Komponenta je totiž často zároveň kontejnerem.

Pro mě Relay na klientu prohrává opět 1:0.

Co s tím?


Pokud se rozhodnete, stejně jako já, že Relay není tou technologií, kterou byste chtěli použít, nabízí se co s tím. Vedle knihovny Relay existuje ještě jedna knihovna a tou je Apollo.

Apollo má výhodu zejména v tom, že je méně invazivní. Nejenom, že vám nabízí, abyste vedle Apolla používali i Redux, ale je i inkrementálně adaptivní. Tedy máte možnost Apollo začít používat i na existujícím projektu, aniž byste museli provést větší refactoring.

Druhou variantou je vlastní řešení.

Vlastní řešení spočívá v jednoduché myšlence. Mám Redux a akce, které ručně vykonávájí načtení dat ze serveru, stejně jako v případě třeba REST API. Prostě si dotazy píšete mimo komponenty a řídíte si sami, co do Reduxu a jak uložíte.

Touto cestou jsem se vydal i já.

Nejenom, že jsem dostatečně flexibilní, ale také jde o to, že samotné dotazy na server jsou velice jednoduché.

Výsledný kód vypadá třeba následovně:

const fetchEdit = (id: string) => {
    return (dispatch: Dispatch<Store>, getState: () => Store) => {
        dispatch({type: LabelActions.FETCHING_LABEL_EDIT});
        const query = `
            query ($id: ID!) {
                label(id: $id) {
                    id
                    name
                    typeId: type {
                        value:id
                        label:key
                    }
                }
            }`;
        client(dispatch, getState)(query, {id}).then((result) => {
            dispatch({type: LabelActions.FETCHED_LABEL_EDIT, payload: result.data.label});
        }).catch((err) => {
            dispatch({type: LabelActions.ERROR_FETCHED_LABEL_EDIT});
            dispatch(MessageActions.setErrorServer(Lang.CONNECTION_ERROR_LABELS_EDIT, err));
        });
    };
};

Metoda načítá data pro editaci:
  1. Nejprve si do reduxu uložím informaci o tom, že dochází k načítání
  2. Poté, co se načtou data, uložím je do reduxu a oznámím, že jsou načtena
  3. V připadě chyby si sám opět zařídím, abych to oznámil do reduxu

Závěr


I přes to, že se dají nalézt výhody, proč se na Relay adaptovat, tak existuje i spoustu důvodů, proč ne. Jedním z hlavních důvodu je to, že pokud se pokusíte použít Relay, bude technologie víc cílem, než cestou. Doufejme, že nová verze, na které se pracuje, bude méně invazivní a umožní i inkrementální adaptaci. Zatím si nechávám Relay v šuplíku a zůstávám u Reduxu s vlastním řešením, které mi vyhovuje víc.

2 komentáře:

  1. const fetchEdit = (id: string) => {
    return (dispatch: Dispatch, getState: () => Store) => {
    dispatch({type: LabelActions.FETCHING_LABEL_EDIT});

    A přesně tato sémantika (nebo syntaxe, nebo jak bych to napsal) mi na javascriptu vadí. Ve zkratce: jeho kód se mi zdá nečitelný :) Asi jako perl oneliners, jako je tohle:

    perl -lne '(1x$_) !~ /^1?$|^(11+?)\1+$/ && print "$_ is prime"'

    OdpovědětVymazat
    Odpovědi
    1. To je spíše jen o zkušenostech s danou syntaxí a funkcionálním programováním. Kód v podstatě říká: "Mám funkci fetchEdit, která vrací funkci, která ma parametr dispatch, což je také funkce a parametr getState, což je také funkce". Zní to šíleně, vypadá to možná šíleně, ale je to efektivní. Navíc, adaptace není tak těžká. Jen to to chce více myslet v lambda výrazech.

      Vymazat

Když programátor založí a řídí firmu

Jako malý jsem chtěl být popelářem. Ani ne tak proto, že bych měl nějaký zvláštní vztah k odpadkům, ale hrozně se mi líbilo, jak...