Как да направите приложението си React напълно функционално, напълно реактивно и в състояние да се справи с всички тези луди странични ефекти

Функционалното реактивно програмиране (FRP) е парадигма, която доби много внимание напоследък, особено в света на предния край на JavaScript. Това е претоварен термин, но описва проста идея:

Всичко трябва да е чисто, така че да е лесно да се тества и разсъждава (функционално), а асинхронното поведение трябва да се моделира, като се използват стойности, които се променят във времето (реактивно).

Реакцията сама по себе си не е напълно функционална, нито е напълно реактивна. Но той е вдъхновен от някои от концепциите зад FRP. Функционалните компоненти например са чисти функции по отношение на техните подпори. И те са реактивни да подкрепят или държавни промени.

Но когато става въпрос за обработка на странични ефекти, React - като само изгледният слой - се нуждае от помощ от други библиотеки, като Redux.

В тази статия ще говоря за редукс-цикли, среден софтуер Redux, който ви помага да боравите със страничните ефекти и асинхронния код във вашите приложения React по функционално-реактивен начин - черта, която все още не е споделена от други модели на странични ефекти Redux - чрез използване на рамката на Cycle.js.

Redux-циклите са както декларативни, така и реактивни

Какви са страничните ефекти?

Страничен ефект променя външния свят. Всичко в приложението ви, което се занимава с отправяне на HTTP заявки, писане до localStorage или дори манипулиране на DOM, се счита за страничен ефект.

Страничните ефекти са лоши. Те са трудни за тестване, сложни за поддръжка и като цяло те са там, където лежат повечето ви бъгове. Вашата цел е следователно да ги минимизирате / локализирате.

Двама програмисти след локализиране на странично ефективен код (източник)
„При наличие на странични ефекти, поведението на програмата зависи от миналата история; тоест има значение редът на оценка. Тъй като разбирането на ефективна програма изисква обмисляне на всички възможни истории, страничните ефекти често правят програмата по-трудна за разбиране. ”- Норман Рамзи

Ето няколко популярни начина за справяне със страничните ефекти в Redux:

  1. redux-thunk - поставя кода на страничните ефекти в създателите на действията
  2. redux-saga - прави логиката на страничните ефекти декларативна, използвайки саги
  3. редукс-наблюдаван - използва реактивно програмиране за моделиране на странични ефекти

Проблемът е, че нито едно от тях не е чисто и реактивно. Някои от тях са чисти (редукс-сага), докато други са реактивни (редукс-наблюдателни), но никой от тях не споделя всички концепции, които въведохме по-рано за FRP.

Redux-циклите са едновременно чисти и реактивни.

Вижте тези слайдове на редукционни цикли от Nick Balestra

Първо ще обясним по-подробно тези функционални и реактивни концепции - и защо трябва да ви пука. След това ще обясним как редукционните цикли действат подробно.

Чисти странични ефекти при работа с Cycle.js

HTTP заявката е може би най-често срещаният страничен ефект. Ето пример за HTTP заявка, използваща redux-thunk:

функция fetchUser (потребител) {
  връщане (изпращане, getState) =>
    извличане ( `https://api.github.com/users/$ {употреба}`)
}

Тази функция е наложителна. Да, връща обещание и можете да го свържете заедно с други обещания, но fetch () осъществява обаждане в този конкретен момент. Не е чисто.

Същото се отнася и за наблюдаване на редукс:

const fetchUserEpic = действие $ =>
  действие $ .ofType (FETCH_USER)
    .mergeMap (действие =>
      ajax.getJSON ( `https://api.github.com/users/$ {action.payload}`)
        .map (fetchUserFulfilled)
    );

ajax.getJSON () прави този фрагмент от код наложително.

За да направите HTTP заявката чиста, не трябва да мислите за „направи HTTP заявка сега“, а по-скоро „нека опиша как искам да изглежда моята HTTP заявка“ и да не се притеснявате кога всъщност се случва или кой я прави.

В Cycle.js това е по същество как кодирате всички неща. Всичко, което правите с рамката, е за създаване на описания за това, което искате да направите. След това тези описания се изпращат към тези неща, наречени драйвери (чрез реактивни потоци), които всъщност се грижат за отправяне на HTTP заявката:

функция основна (източници) {
  const заявка $ = xs.of ({
    url: `https: // api.github.com / users / foo`,
  });
  връщане {
    HTTP: заявка $
  };
}

Както можете да видите от този фрагмент от код, няма функционално обаждане за действително отправяне на заявката. Ако стартирате този код, ще видите заявката да се случи независимо. И какво всъщност се случва зад кулисите?

Магията се случва благодарение на шофьорите. Cycle.js знае, че когато вашата функция връща обект с HTTP ключ, той трябва да обработва съобщенията, които получава от този поток, и съответно да извърши HTTP заявка (чрез HTTP драйвер).

Драйверите ви позволяват да боравите със страничните ефекти по чист начин.

Ключовият момент е, че не сте се отървали от страничния ефект - HTTP заявката все още трябва да се случи - но сте я локализирали извън кода на приложението си.

Вашите функции са много по-лесни за разсъждения и са особено много по-лесни за тестване, защото можете просто да тествате дали функциите ви излъчват правилните съобщения - не са необходими странни подигравки или времето.

Реактивни странични ефекти

В по-ранните примери засегнахме реактивността. Трябва да има начин да общувате с тези така наречени шофьори за „правенето на неща във външния свят“ и да бъдете уведомявани за „неща, които се случват във външния свят“.

Наблюдаваните (известни още като потоци) са перфектната абстракция за този вид асинхронна комуникация.

Всеки път, когато искате да „направите нещо“, излъчвате на изходен поток описание на това, което искате да направите. Тези изходни потоци се наричат ​​потъвачи в света Cycle.js.

Всеки път, когато искате да бъдете уведомени за нещо, което се е случило, използвате входен поток (наречен източници) и просто картографирате стойностите на потока, за да научите за случилото се.

Това образува един вид реактивен цикъл, който изисква различно мислене, за да се разбере от нормалния императивен код. Нека моделираме HTTP заявка / отговор на жизнения цикъл, използвайки тази парадигма:

функция основна (източници) {
  const отговор $ = source.HTTP
    .Изберете ( "Фу)
    .flatten ()
    .map (отговор => отговор);
  const заявка $ = xs.of ({
    url: `https: // api.github.com / users / foo`,
    категория: „foo“,
  });
  const мивки = {
    HTTP: заявка $
  };
  възвратни мивки;
}

HTTP драйверът знае за HTTP ключа, върнат от тази функция. Това е поток, съдържащ описание на HTTP заявка за URL адрес на GitHub. Той казва на HTTP драйвера: „Искам да направя заявка към този URL адрес“.

След това драйверът знае да изпълни заявката и изпраща отговора обратно към основната функция като източник (source.HTTP) - обърнете внимание, че потъващите и източниците използват един и същ обект ключ.

Нека да обясним това отново: използваме source.HTTP, за да бъдем „уведомени за HTTP отговорите“. И ние връщаме sinks.HTTP да „правим HTTP заявки“.

За да обясните този важен реактивен цикъл, ето една анимация:

Реактивен цикъл между приложението ви и външния свят

Това изглежда контраинтуитивно в сравнение с нормалното императивно програмиране: защо кодът за четене на отговора съществува преди кода, отговорен за заявката?

Това е така, защото няма значение къде е кодът в FRP. Всичко, което трябва да направите, е да изпращате описания и да слушате промени. Подредбата на кода не е важна.

Това позволява много лесно рефакторинг на код.

Въвеждане на редукционни цикли

Redux-cycles е комбинация от Redux и Cycle.js

В този момент може би се питате, какво общо има всичко това с моето приложение React?

Научихте за предимствата на чистотата на кода, като напишете само описания на това, което искате да направите. И сте научили за предимствата на използването на Observables за комуникация с външния свят.

Сега ще видите как да използвате тези концепции в съществуващите си приложения React, за да станат напълно функционални и реактивни.

Прихващане и изпращане на действията на Redux

С Redux изпращате действия, за да кажете на вашите редуктори, че искате ново състояние.

Този поток е синхронен, което означава, че ако искате да въведете асинхронно поведение (за странични ефекти), трябва да използвате някаква форма на междинен софтуер, който прихваща действия, прави асинхронния страничен ефект и изпраща други действия съответно.

Точно това правят редукционните цикли. Това е междинен софтуер, който прихваща редукционни действия, влиза в реактивния цикъл на Cycle.js и ви позволява да извършвате други странични ефекти, като използвате други драйвери. След това изпраща нови действия въз основа на потока от данни за асинхронизация, описан във вашите функции:

функция основна (източници) {
  const заявка $ = source.ACTION
    .filter (action => action.type === FETCH_USER)
    .map (действие => ({
      url: `https://api.github.com/users/$ {action.payload}`,
      категория: „потребители“,
    }));

  const action $ = source.HTTP
    .Изберете ( "потребители")
    .flatten ()
    .map (fetchUserFulfilled);

  const мивки = {
    ДЕЙСТВИЕ: действие $,
    HTTP: заявка $
  };
  възвратни мивки;
}

В горния пример има нов източник и мивка, въведени чрез редукционни цикли - ДЕЙСТВИЕ. Но комуникационната парадигма е същата.

Той слуша действия, които се изпращат от света на Redux, използвайки източници.ACTION. И изпраща нови действия към света Redux, като връща мивки.ACTION.

По-специално той излъчва стандартни обекти на Flux Actions.

Хубавото е, че можете да комбинирате неща, които се случват от други драйвери. В по-ранния пример нещата, които се случват в света на HTTP, всъщност предизвикват промени в света на ДЕЙСТВИЕТО и обратно.

- Обърнете внимание, че общуването с Redux се осъществява изцяло чрез източника / мивката на ACTION. Шофьорите на Redux-cycles се справят с действителната доставка за вас.

Как различни драйвери си взаимодействат помежду си

Ами по-сложните приложения?

Как се развива по-сложни приложения, ако просто пишете чисти функции, които трансформират потоци от данни?

Оказва се, че можете да направите почти всичко, като използвате вече изградени драйвери. Или можете лесно да създадете свой собствен - ето ви един прост драйвер, който записва съобщения, записани в неговата мивка.

тичам (главно, {
  LOG: msg $ => msg $ .addListener ({
    следващ: msg => console.log (msg)
  })
});

run е част от Cycle.js, която изпълнява вашата основна функция (първи аргумент) и преминава покрай всички драйвери (втори аргумент).

Redux-cycles представя два драйвера, които ви позволяват да общувате с Redux; makeActionDriver () & makeStateDriver ():

import {createCycleMiddleware} от 'редукс-цикли';
const cycleMiddleware = createCycleMiddleware ();
const {makeActionDriver, makeStateDriver} = cycleMiddleware;
const store = createStore (
  rootReducer,
  applyMiddleware (cycleMiddleware)
);
тичам (главно, {
  ДЕЙСТВИЕ: makeActionDriver (),
  СЪСТОЯНИЕ: makeStateDriver ()
})

makeStateDriver () е драйвер само за четене. Това означава, че можете да четете само source.STATE в основната си функция. Не можете да го кажете какво да прави; можете да четете само данни от него.

Всеки път, когато състоянието Redux се промени, потокът thesources.STATE ще излъчва новия обект на състояние. Това е полезно, когато трябва да напишете конкретна логика въз основа на текущото състояние на приложението.

Redux и Cycle.js се държат отделно. Те комуникират само чрез драйвери за редукционни цикли.

Сложен поток от асинхронизиране на данни

Наблюденията идват с оператори, което ви позволява да изграждате сложни асинхронни потоци

Друго голямо предимство на реактивното програмиране е възможността да се използват оператори за съставяне на потоци в други потоци - ефективно ги третират като масиви от стойности във времето: можете да ги картографирате, филтрирате и дори да намалите.

Операторите правят възможни явни графики на потока от данни; т.е. разсъждение на зависимостите между операциите. Позволява ви да визуализирате данни, преминаващи през различни оператори, като анимацията по-горе.

Redux-наблюдаем също ви позволява да пишете сложни асинхронни потоци - те използват мултиплексен пример WebSocket като своя продажна точка - обаче силата на писането на тези потоци по чист начин е това, което наистина разделя Cycle.js.

Тъй като всичко е чист поток от данни, можем да си представим бъдеще, в което програмирането няма да бъде нищо друго освен свързване на блокове от оператори.

Тестване с мраморни диаграми

Мраморна диаграма. Всяка стрелка представлява поток. Всеки кръг е стойност, излъчена в този поток.

Не на последно място идва тестване. Тук наистина свети редукс-цикли (и като цяло всички приложения на Cycle.js).

Тъй като в кода на приложението ви всичко е чисто, за да тествате основната си функция, просто му давате потоци като вход и очаквате конкретни потоци като изход.

С помощта на прекрасния проект @ цикъл / време можете дори да рисувате мраморни диаграми и да тествате функциите си по много визуален начин:

assertSourcesSinks ({
  ДЕЙСТВИЕ: {'-a-b-c ---- |': actionSource},
  HTTP: {'--- r ------ |': httpSource},
}, {
  HTTP: {'--------- r |': httpSink},
  ДЕЙСТВИЕ: {'--- a ------ |': actionSink},
}, searchUsers, готово);

Този къс код изпълнява функцията searchUsers, като му предава специфични източници като вход (първи аргумент). Като се имат предвид тези източници, тя очаква функцията да върне предоставените потъвания (втори аргумент). Ако не стане, твърдението се проваля.

Определянето на потоци графично по този начин е особено полезно, когато трябва да тествате асинхронно поведение.

Когато източникът на HTTP излъчва r (отговор), веднага очаквате да се появи (действие) в мивката ACTION - те се случват едновременно. Въпреки това, когато източникът на ДЕЙСТВИЕ излъчва сноп от -a-b-c, не очаквате да се появи нещо в този момент в HTTP мивката.

Това е така, защото searchUsers има за цел да разобличи действията, които получава. Той ще изпрати HTTP заявка само след 800 милисекунди неактивност в потока ACTION източник: той осъществява функция за автоматично завършване.

Тестване на този вид асинхронно поведение е тривиално с чисти и реактивни функции.

заключение

В тази статия обяснихме истинската сила на FRP. Представихме Cycle.js и неговите парадигми за роман. Страхотният списък Cycle.js е важен ресурс, ако искате да научите повече за тази технология.

Използването на Cycle.js самостоятелно - без React или Redux - изисква малко промяна в манталитета, но може да се направи, ако желаете да изоставите някои от технологиите и ресурсите в общността React / Redux.

Redux-cycles от друга страна ви позволява да продължите да използвате всички страхотни React неща, докато си намокрите ръцете с FRP и Cycle.js.

Специални благодарности на Гоша Аринич и Ник Балестра за поддържането на проекта заедно със мен, както и на Ник Джонстоун за доказателството, четейки тази статия.