Jak zajistit, aby vaše aplikace React byla plně funkční, plně reaktivní a schopná zvládnout všechny ty bláznivé vedlejší účinky

Funkční reaktivní programování (FRP) je paradigma, které si v poslední době získalo hodně pozornosti, zejména ve světě front-endů JavaScript. Je to přetížený termín, ale popisuje jednoduchý nápad:

Všechno by mělo být čisté, aby bylo snadné testovat a uvažovat o (funkčním), a asynchronní chování by mělo být modelováno pomocí hodnot, které se v průběhu času mění (reaktivní).

Samotná reakce není plně funkční, ani není plně reaktivní. Inspirována je však některými koncepty, které stojí za FRP. Funkční komponenty jsou například čistými funkcemi s ohledem na jejich rekvizity. A jsou reaktivní na změny stavu nebo stavu.

Ale pokud jde o řešení vedlejších účinků, React - jen vrstva pohledu - potřebuje pomoc od jiných knihoven, jako je Redux.

V tomto článku budu hovořit o redux-cyklech, což je middleware Redux, který vám pomůže zvládnout vedlejší účinky a asynchronní kód v aplikacích React funkčně reaktivním způsobem - vlastnost, kterou dosud nesdílejí jiné modely vedlejších efektů Redux - využitím rámce Cycle.js.

Reduxové cykly jsou deklarativní i reaktivní

Jaké jsou vedlejší účinky?

Vedlejší účinek modifikuje vnější svět. Všechno ve vaší aplikaci, které se zabývá vytvářením požadavků HTTP, zápisem do localStorage nebo dokonce manipulací s DOM, je považováno za vedlejší účinek.

Vedlejší účinky jsou špatné. Je těžké je testovat, je obtížné je udržovat a obvykle se nacházejí tam, kde leží většina vašich chyb. Vaším cílem je proto je minimalizovat / lokalizovat.

Dva programátoři po lokalizaci efektivního vedlejšího kódu (zdroj)
"V přítomnosti vedlejších účinků závisí chování programu na minulé historii; to znamená, že na pořadí hodnocení záleží. Protože pochopení efektivního programu vyžaduje přemýšlení o všech možných dějinách, vedlejší účinky často ztíží pochopení programu. “- Norman Ramsey

Zde je několik oblíbených způsobů, jak zvládnout vedlejší účinky v Reduxu:

  1. redux-thunk - vloží kód vedlejších účinků do tvůrců akcí
  2. redux-saga - učiní vaše vedlejší účinky logickými deklarativními pomocí ság
  3. redux-pozorovatelný - používá reaktivní programování k modelování vedlejších účinků

Problém je v tom, že žádný z nich není čistý ani reaktivní. Některé z nich jsou čisté (redux-saga), zatímco jiné jsou reaktivní (redux-pozorovatelné), ale žádný z nich nesdílí všechny koncepty, které jsme dříve o FRP představili.

Reduxové cykly jsou čisté i reaktivní.

Poklepejte na tyto snímky s opakovaným cyklem od Nicka Balestry

Nejprve podrobněji vysvětlíme tyto funkční a reaktivní koncepty - a proč byste se měli starat. Poté podrobně vysvětlíme, jak reduxcykly fungují.

Čisté vedlejší účinky s Cycle.js

Požadavek HTTP je pravděpodobně nejběžnějším vedlejším účinkem. Zde je příklad požadavku HTTP pomocí redux-thunk:

function fetchUser (user) {
  návrat (odeslání, getState) =>
    načíst (`https://api.github.com/users/$ {user}`)
}

Tato funkce je nezbytná. Ano, je to příslib a můžete ho zřetězit spolu s dalšími sliby, ale fetch () telefonuje, v tomto konkrétním okamžiku v čase. Není to čisté.

Totéž platí pro opakované pozorování:

const fetchUserEpic = akce $ =>
  akce $ .ofType (FETCH_USER)
    .mergeMap (action =>
      ajax.getJSON (`https://api.github.com/users/$ {action.payload}`)
        .map (fetchUserFulfilled)
    );

ajax.getJSON () činí tento fragment kódu nezbytným.

Aby byl požadavek HTTP čistý, neměli byste myslet na „vytvořit požadavek HTTP nyní“, ale raději „dovolte mi popsat, jak chci, aby můj požadavek HTTP vypadal“, a nestarat se o to, kdy k tomu skutečně dojde nebo kdo ho vytvoří.

V Cycle.js je to v podstatě způsob, jak kódovat všechny věci. Vše, co s rámcem děláte, je o vytváření popisů toho, co chcete dělat. Tyto popisy jsou pak zasílány na tyto věci zvané ovladače (prostřednictvím reaktivních toků), které se skutečně starají o provedení požadavku HTTP:

funkce main (zdroje) {
  požadavek na ústavu $ = xs.of ({
    url: `https: // api.github.com / users / foo`,
  });
  vrátit se {
    HTTP: požadavek $
  };
}

Jak můžete vidět z tohoto úryvku kódu, neexistuje žádné volání funkce, které by skutečně provedlo požadavek. Pokud spustíte tento kód, zobrazí se žádost bez ohledu na to. Co se vlastně děje v zákulisí?

Kouzlo se děje díky řidičům. Cycle.js ví, že když vaše funkce vrátí objekt pomocí klíče HTTP, musí zpracovat zprávy, které obdrží z tohoto proudu, a podle toho provést požadavek HTTP (prostřednictvím ovladače HTTP).

Ovladače vám umožňují zvládnout vedlejší účinky čistě.

Klíčovým bodem je, že jste se nezbavili vedlejšího efektu - požadavek HTTP se musí ještě uskutečnit - lokalizovali jste jej však mimo kód aplikace.

O vašich funkcích je mnohem jednodušší důvod, a zejména je mnohem snazší otestovat, protože si můžete jednoduše vyzkoušet, zda vaše funkce vysílají správné zprávy - není potřeba divný výsměch nebo načasování.

Reaktivní vedlejší účinky

V předchozích příkladech jsme se dotkli reaktivity. Musí existovat způsob, jak komunikovat s těmito takzvanými řidiči o „dělání věcí ve vnějším světě“ a být informován o „věcech, které se dějí ve vnějším světě“.

Observables (aka streams) jsou perfektní abstrakce pro tento druh asynchronní komunikace.

Kdykoli chcete „něco udělat“, vysíláte výstupnímu proudu popis toho, co chcete dělat. Tyto výstupní toky se ve světě Cycle.js nazývají dřezy.

Kdykoli chcete být „upozorněni na něco, co se stalo“, použijte vstupní tok (nazývaný zdroje) a jednoduše namapujte hodnoty proudu a zjistěte, co se stalo.

To vytváří určitý druh reaktivní smyčky, která vyžaduje pochopení odlišného myšlení než normální imperativní kód. Modelujme životní cyklus požadavků HTTP / odpovědí pomocí tohoto paradigmatu:

funkce main (zdroje) {
  const response $ = sources.HTTP
    .select ('foo')
    .flatten ()
    .map (response => response);
  požadavek na ústavu $ = xs.of ({
    url: `https: // api.github.com / users / foo`,
    kategorie: 'foo',
  });
  const sinks = {
    HTTP: požadavek $
  };
  zpětné umyvadla;
}

Ovladač HTTP ví o klíči HTTP vráceném touto funkcí. Je to stream obsahující popis požadavku HTTP pro adresu URL GitHub. Říká ovladači HTTP: „Chci na tuto adresu URL požádat“.

Ovladač potom ví, že požadavek provede, a odešle odpověď zpět do hlavní funkce jako zdroj (sources.HTTP) - všimněte si, že umyvadla a zdroje používají stejný klíč objektu.

Vysvětlíme to znovu: používáme sources.HTTP pro „upozornění na odpovědi HTTP“. A vracíme sinks.HTTP, aby „vytvářel HTTP požadavky“.

Vysvětlení této důležité reaktivní smyčky je animace:

Reaktivní smyčka mezi vaší aplikací a okolním světem

Ve srovnání s běžným imperativním programováním se to jeví jako kontraintuitivní: proč by měl kód pro čtení odpovědi existovat před kódem odpovědným za žádost?

Je to proto, že nezáleží na tom, kde je kód v FRP. Jediné, co musíte udělat, je poslat popisy a poslouchat změny. Pořadí kódu není důležité.

To umožňuje velmi snadné refaktorování kódu.

Představujeme opakované cykly

Redux-cykly jsou kombinací Redux a Cycle.js

V tuto chvíli se možná ptáte, co to má společného s mou aplikací React?

Dozvěděli jste se o výhodách, díky kterým je váš kód čistý, a to pouze psáním popisů toho, co chcete dělat. A dozvěděli jste se o výhodách používání Observables ke komunikaci s okolním světem.

Nyní uvidíte, jak používat tyto koncepty ve stávajících aplikacích React, aby ve skutečnosti fungovaly plně funkční a reaktivní.

Zachytávání a odesílání akcí Redux

S Reduxem odesíláte akce, abyste řekli reduktorům, že chcete nový stav.

Tento tok je synchronní, což znamená, že pokud chcete zavést asynchronní chování (pro vedlejší účinky), musíte použít nějakou formu middlewaru, která zachycuje akce, provádí asynchronní vedlejší efekt a podle toho vydává další akce.

To je přesně to, co opakovací cykly dělá. Je to middleware, který zachycuje opakované akce, vstupuje do reaktivní smyčky Cycle.js a umožňuje provádět další vedlejší účinky pomocí jiných ovladačů. Poté odešle nové akce na základě asynchronního datového toku popsaného ve vašich funkcích:

funkce main (zdroje) {
  požadavek na $ $ = sources.ACTION
    .filter (action => action.type === FETCH_USER)
    .map (action => ({
      url: `https://api.github.com/users/$ {action.payload}`,
      kategorie: 'uživatelé',
    }));

  const action $ = sources.HTTP
    .select ('users')
    .flatten ()
    .map (fetchUserFulfilled);

  const sinks = {
    AKCE: akce $,
    HTTP: požadavek $
  };
  zpětné umyvadla;
}

Ve výše uvedeném příkladu je nový zdroj a umyvadlo zavedeno opakovanými cykly - AKCE. Komunikační paradigma je však stejná.

Poslouchá akce odesílané ze světa Redux pomocí sources.ACTION. A vysílá nové akce do světa Redux návratem dřezů.

Konkrétně emituje standardní objekty Flux Actions.

Skvělá věc je, že můžete kombinovat věci, které se dějí od jiných ovladačů. V předchozím příkladu věci, které se dějí ve světě HTTP, skutečně vedou ke změnám ve světě AKCE a naopak.

- Všimněte si, že komunikace s Reduxem probíhá výhradně prostřednictvím zdroje / dřezu ACTION. Řidiči Redux-cyklů zvládnou skutečný dispečink za vás.

Jak různé ovladače vzájemně spolupracují

A co složitější aplikace?

Jak lze vyvinout složitější aplikace, pokud píšete čistě funkce, které transformují datové proudy?

Ukázalo se, že pomocí již postavených ovladačů můžete udělat téměř cokoli. Nebo si můžete snadno vytvořit svůj vlastní - zde je jednoduchý ovladač, který zaznamenává zprávy zapsané do jeho dřezu.

run (main, {
  LOG: msg $ => msg $ .addListener ({
    next: msg => console.log (msg)
  })
});

run je součástí Cycle.js, který spouští vaši hlavní funkci (první argument) a předává všechny ovladače (druhý argument).

Cykly Redux představují dva ovladače, které umožňují komunikaci s Reduxem; makeActionDriver () a makeStateDriver ():

importovat {createCycleMiddleware} z 'redux-cycle';
const cycleMiddleware = createCycleMiddleware ();
const {makeActionDriver, makeStateDriver} = cycleMiddleware;
const store = createStore (
  rootReducer,
  applyMiddleware (cycleMiddleware)
);
run (main, {
  AKCE: makeActionDriver (),
  STÁT: makeStateDriver ()
})

makeStateDriver () je ovladač jen pro čtení. To znamená, že ve své hlavní funkci můžete číst pouze zdroje. Nemůžete říct, co dělat; můžete z nich pouze číst data.

Pokaždé, když se změní stav Redux, vysílá proud thesources.STATE nový stavový objekt. To je užitečné, když potřebujete napsat konkrétní logiku založenou na aktuálním stavu aplikace.

Redux a Cycle.js jsou odděleny. Komunikují pouze prostřednictvím ovladačů redux-cycle.

Složitý tok asynchronních dat

Pozorovatelné přicházejí s operátory, což vám umožňuje vytvářet komplexní asynchronní toky

Další velkou výhodou reaktivního programování je schopnost operátorů skládat toky do jiných toků - efektivně s nimi zacházet jako s poli hodnot v průběhu času: můžete je mapovat, filtrovat a dokonce je redukovat.

Operátoři umožňují explicitní grafy toku dat; tj. zdůvodnění závislostí mezi operacemi. Umožní vám vizualizovat data tekoucí prostřednictvím různých operátorů, jako je výše uvedená animace.

Redux-pozorovatelné také umožňuje psát složité asynchronní toky - jako prodejní místo používají příklad multiplexního WebSocket - nicméně moc psaní těchto toků čistě je to, co skutečně Cycle.js odděluje.

Protože všechno je čistě datový tok, můžeme si představit budoucnost, kde programování nebude nic jiného než propojení bloků operátorů.

Testování s mramorovými diagramy

Mramorový diagram. Každá šipka představuje proud. Každá kružnice je hodnota emitovaná v tomto proudu.

V neposlední řadě přichází testování. Zde opravdu svítí opakované cykly (a obecně všechny aplikace Cycle.js).

Protože je ve vašem kódu aplikace vše čisté, pro otestování hlavní funkce jednoduše dáte toky jako vstup a očekáváte konkrétní toky jako výstup.

Pomocí nádherného projektu @ cycle / time můžete dokonce kreslit mramorové diagramy a otestovat své funkce velmi vizuálním způsobem:

assertSourcesSinks ({
  AKCE: {'-a-b-c ---- |': actionSource},
  HTTP: {'--- r ------ |': httpSource},
}, {
  HTTP: {'--------- r |': httpSink},
  AKCE: {'--- a ------ |': actionSink},
}, searchUsers, done);

Tento kus kódu provádí funkci searchUsers a předává jí konkrétní zdroje jako vstup (první argument). Vzhledem k těmto zdrojům očekává, že funkce vrátí poskytnuté dřezy (druhý argument). Pokud tomu tak není, tvrzení selže.

Grafické definování toků tímto způsobem je zvláště užitečné, když potřebujete otestovat asynchronní chování.

Když zdroj HTTP vysílá r (odpověď), okamžitě očekáváte, že se v dřezu AKCE objeví (akce) - vyskytují se současně. Když však zdroj AKCE vydá výbuch -a-b-c, neočekáváte, že se v té chvíli v jímce HTTP objeví něco.

Důvodem je, že searchUsers má za cíl zbavit činnosti, které přijme. Požadavek HTTP odešle až po 800 milisekundách nečinnosti ve zdrojovém proudu ACTION: implementuje funkci automatického doplňování.

Testování tohoto druhu asynchronního chování je triviální s čistými a reaktivními funkcemi.

Závěr

V tomto článku jsme vysvětlili skutečnou sílu FRP. Představili jsme Cycle.js a jeho nová paradigma. Úžasný seznam Cycle.js je důležitým zdrojem, pokud se chcete o této technologii dozvědět více.

Používání Cycle.js samostatně - bez React nebo Redux - vyžaduje trochu změny v mentalitě, ale může být provedeno, pokud jste ochotni opustit některé technologie a zdroje v komunitě React / Redux.

Opakované cykly na druhé straně vám umožňují pokračovat v používání všech skvělých věcí React a zároveň navlhčit ruce pomocí FRP a Cycle.js.

Zvláštní poděkování patří Goshovi Arinichovi a Nicku Balestrovi za zachování projektu spolu se mnou a Nicku Johnstoneovi za důkaz, že si tento článek přečetli.