Реактивный X Redux

Каждый, кто работает с Redux, рано или поздно сталкивается с проблемой асинхронных действий.

Но без них невозможно разработать современное приложение.

К ним относятся http-запросы к бэкэнду и всевозможные таймеры/задержки.

Сами создатели Redux говорят однозначно — по умолчанию поддерживается только синхронный поток данных, все асинхронные действия необходимо размещать в middleware. Конечно, это слишком многословно и неудобно, поэтому сложно найти разработчика, который использует только «родное» промежуточное ПО.

На помощь всегда приходят библиотеки и фреймворки, такие как Thunk, Saga и им подобные.

Для большинства задач их вполне достаточно.

Но что, если вам нужна немного более сложная логика, чем отправка одного запроса или создание одного таймера? Вот небольшой пример:

  
  
  
  
  
  
  
  
  
  
  
  
   

async dispatch => { setTimeout(() => { try { await Promise .

all([fetchOne, fetchTwo]) .

then(([respOne, respTwo]) => { dispatch({ type: 'SUCCESS', respOne, respTwo }); }); } catch (error) { dispatch({ type: 'FAILED', error }); } }, 2000); }

На такой код больно даже смотреть, а поддерживать и расширять его просто невозможно.

Что делать, если вам нужна более сложная обработка ошибок? Что делать, если вам нужно повторить запрос? Что если я захочу повторно использовать эту функцию? Меня зовут Дмитрий Самохвалов, и в этом посте я расскажу, что такое концепция Observable и как ее применять на практике в связке с Redux, а также сравню все это с возможностями Redux-Saga. Как правило, в таких случаях берут редукс-сагу.

Хорошо, давайте перепишем это в саги:

try { yield call(delay, 2000); const [respOne, respTwo] = yield [ call(fetchOne), call(fetchTwo) ]; yield put({ type: 'SUCCESS', respOne, respTwo }); } catch (error) { yield put({ type: 'FAILED', error }); }

Стало заметно лучше — код почти линеен, лучше выглядит и читается.

Но его по-прежнему сложно расширять и повторно использовать, потому что сага — такой же обязательный инструмент, как и thunk. Есть другой подход. Это именно тот подход, а не очередная библиотека для написания асинхронного кода.

Он называется Rx (он же Observables, Reactive Streams и т. д.).

Давайте воспользуемся им и перепишем пример на Observable:

action$ .

delay(2000) .

switchMap(() => Observable.merge(fetchOne, fetchTwo) .

map(([respOne, respTwo]) => ({ type: 'SUCCESS', respOne, respTwo })) .

catch(error => ({ type: 'FAILED', error }))

Код не только стал более плоским и уменьшился в объёме, изменился сам принцип описания асинхронных действий.

Теперь мы не работаем напрямую с запросами, а выполняем операции над специальными объектами под названием Observable. Удобно думать о Observable как о функции, возвращающей поток (последовательность) значений.

Observable имеет три основных состояния — next («выдать следующее значение»), error («произошла ошибка») и Complete («значения закончились, выдать больше нечего»).

В этом плане он немного похож на Promise, но отличается тем, что по этим значениям можно перебирать (и это одна из суперспособностей Observable).

Вы можете обернуть в Observable что угодно — таймауты, HTTP-запросы, события DOM, просто объекты js.

Реактивный X Redux

Вторая суперсила Observable — это ее операторы.

Оператор — это функция, которая принимает и возвращает Observable, но выполняет некоторые действия с потоком значений.

Ближайшая аналогия — map и filter из javascript (кстати, такие операторы есть в Rx).



Реактивный X Redux

Лично для меня самыми полезными операторами оказались zip, forkJoin и FlatMap. На их примере проще всего объяснить работу операторов.

Оператор zip работает очень просто — он принимает на вход несколько Observables (не более 9) и возвращает выдаваемые ими значения в виде массива.



const first = fromEvent("mousedown"); const second = fromEvent("mouseup"); zip(first, second) .

subscribe(e => console.log(`${e[0].

x} ${e[1].

x}`)); //output [119,120] [120,233] …

В целом работу zip можно представить следующей схемой:

Реактивный X Redux

Zip используется, если у вас есть несколько Observable и вам необходимо последовательно получать от них значения (даже если они могут генерироваться с разными интервалами, синхронно или нет).

Это очень полезно при работе с событиями DOM. Оператор forkJoin похож на zip с одним исключением — он возвращает только последние значения из каждого Observable.

Реактивный X Redux

Соответственно, его разумно использовать, когда нужны только конечные значения из потока.

Оператор FlatMap немного сложнее.

Он принимает Observable в качестве входных данных, возвращает новый Observable и отображает значения из него в новый Observable, используя либо функцию выбора, либо другой Observable. Звучит запутанно, но схема довольно проста:

Реактивный X Redux

Еще более четко в коде:

const observable = of("Hello"); const promise = value => new Promise(resolve => resolve(`${value} World`); observable .

flatMap(value => promise(value)) .

subscribe(result => console.log(result)); //output "Hello World"

FlatMap чаще всего используется в бэкэнд-запросах наряду с switchMap и concatMap. Как вы можете использовать Rx в Redux? Для этого есть замечательная библиотека, наблюдаемая при сокращении.

Его архитектура выглядит так:

Реактивный X Redux

Все Observable, операторы и действия над ними спроектированы в виде специального промежуточного программного обеспечения, называемого эпиком.

Каждый эпик принимает действие в качестве входных данных, оборачивает его в Observable и должен возвращать действие, также как Observable. Невозможно вернуть обычное действие, это создает бесконечный цикл.

Напишем небольшую эпопею, которая делает запрос к API.

const fetchEpic = action$ => action$ .

ofType('FETCH_INFO') .

map(() => ({ type: 'FETCH_START' })) .

flatMap(() => Observable .

from(apiRequest) .

map(data => ({ type: 'FETCH_SUCCESS', data })) .

catch(error => ({ type: 'FETCH_ERROR', error })) )

Невозможно обойтись без сравнения redux-observable и redux-saga. Многие думают, что они близки по функционалу и возможностям, но это совсем не так.

Саги — это совершенно императивный инструмент, по сути, набор методов работы с побочными эффектами.

Observable — это принципиально другой стиль написания асинхронного кода, другая философия, если хотите.

Я написал несколько примеров, иллюстрирующих возможности и подход к решению задач.

Допустим, нам нужно реализовать таймер, который будет останавливаться в зависимости от действия.

Вот как это выглядит в сагах:

while(true) { const timer = yield race({ stopped: take('STOP'), tick: call(wait, 1000) }) if (!timer.stopped) { yield put(actions.tick()) } else { break } }

Теперь используем Rx:

interval(1000) .

takeUntil(action$.

ofType('STOP'))

Допустим, есть задача реализовать запрос с отменой в сагах:

function* fetchSaga() { yield call(fetchUser); } while (yield take('FETCH')) { const fetchSaga = yield fork(fetchSaga); yield take('FETCH_CANCEL'); yield cancel(fetchSaga); }

На Rx все проще:

switchMap(() => fetchUser()) .

takeUntil(action$.

ofType('FETCH_CANCEL'))

Наконец-то мой фаворит. Реализовать запрос к API; в случае неудачи сделать не более 5 повторных запросов с задержкой в 2 секунды.

Вот что мы имеем в сагах:

for (let i = 0; i < 5; i++) { try { const apiResponse = yield call(apiRequest); return apiResponse; } catch (err) { if(i < 4) { yield delay(2000); } } } throw new Error(); }

Что будет на Rx:

.

retryWhen(errors => errors .

delay(1000) .

take(5))

Если суммировать плюсы и минусы саги, то получится следующая картина:

Реактивный X Redux

Саги просты в освоении и очень популярны, поэтому в сообществе можно найти рецепты практически на все случаи жизни.

К сожалению, императивный стиль затрудняет по-настоящему гибкое использование саг.

Совершенно иная ситуация с Rx:

Реактивный X Redux

Может показаться, что Rx — это волшебный молот и серебряная пуля.

К сожалению, это не так.

Порог входа в Rx заметно выше, поэтому сложнее ввести нового человека в проект, который активно использует Rx. Кроме того, при работе с Observables особенно важно быть осторожным и всегда хорошо понимать, что происходит. В противном случае вы можете наткнуться на неочевидные ошибки или неопределенное поведение.



action$ .

ofType('DELETE') .

switchMap(() => Observable .

fromPromise(deleteRequest) .

map(() => ({ type: 'DELETE_SUCCESS'})))

Однажды я написал эпик, который выполнял довольно простую работу — каждое действие типа «DELETE» вызывало метод API, который удалял элемент. Однако во время тестирования возникли проблемы.

Тестировщик жаловался на странное поведение — иногда при нажатии кнопки удаления ничего не происходило.

Оказалось, что оператор switchMap поддерживает одновременное выполнение только одного Observable, своего рода защита от состояния гонки.

В итоге дам несколько рекомендаций, которым следую сам и призываю следовать всем, кто начинает работать с Rx:

  • Будь осторожен.

  • Изучите документацию.

  • Проверьте в песочнице.

  • Пишите тесты.

  • Не стреляйте воробьев из пушки.

Теги: #программирование #JavaScript #react.js #react #saga #redux #redux-saga #observable #thunk #ReactiveX Redux
Вместе с данным постом часто просматривают:

Автор Статьи


Зарегистрирован: 2019-12-10 15:07:06
Баллов опыта: 0
Всего постов на сайте: 0
Всего комментарий на сайте: 0
Dima Manisha

Dima Manisha

Эксперт Wmlog. Профессиональный веб-мастер, SEO-специалист, дизайнер, маркетолог и интернет-предприниматель.