Конечно, это слишком многословно и неудобно, поэтому тяжело найти разработчика, который пользуется одними только “нативными” 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.
Как правило, в таких случаях берут 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 объекты.

Второй суперсилой Observable являются операторы. Оператор — это функция, которая принимает и возвращает Observable, но производит какие-то действия над потоком значений. Ближайшая аналогия — map и filter из javascript (кстати, такие операторы есть в Rx).

Наиболее полезными лично для меня были операторы zip, forkJoin и flatMap. На их примере легче всего объяснить работу операторов.
Оператор zip работает очень просто — он принимает на вход несколько Observable (не более 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 можно представить схемой:

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

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

Еще нагляднее в коде:
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? Для этого есть замечательная библиотека redux-observable. Ее архитектура выглядит так:

Все Observable, операторы и действия над ними оформляются в виде специального middleware, который называется epic. Каждый epic принимает на вход action, оборачивает его в Observable и должен вернуть action, также в виде Observable. Возвращать обычный action нельзя, это создает бесконечный цикл. Напишем небольшой epic, который делает запрос к апи.
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'))
Напоследок мое любимое. Реализовать запрос к апи, в случае неудачи сделать не более 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))
Если суммировать плюсы и минусы саги, получится такая картина:

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

Может показаться, что Rx это волшебный молоток и серебряная пуля. К сожалению, это не так. Порог входа в Rx заметно выше, поэтому тяжелее вводить нового человека в проект, активно использующий Rx.
Кроме того, при работе с Observable особенно важно быть внимательным и всегда хорошо понимать, что происходит. Иначе можно наткнуться на неочевидные ошибки или неопределенное поведение.
action$ .ofType('DELETE') .switchMap(() => Observable .fromPromise(deleteRequest) .map(() => ({ type: 'DELETE_SUCCESS'})))
Однажды я написал epic, который делал довольно простую работу — при каждом action с типом ‘DELETE’ вызывался метод API, который производил удаление элемента. Однако при тестировании возникли проблемы. Тестировщик жаловался на странное поведение — иногда при нажатии на кнопку удаления не происходило ничего. Оказалось, что оператор switchMap поддерживает выполнение только одного Observable в момент времени, своего рода защита от race condition.
В качестве итога приведу несколько рекомендаций, которым следую сам и призываю следовать всем, кто начинает работу с Rx:
- Будьте внимательны.
- Изучайте документацию.
- Проверяйте в sandbox.
- Пишите тесты.
- Не стреляйте из пушки по воробьям.
ссылка на оригинал статьи https://habr.com/ru/company/alfa/blog/460155/
Добавить комментарий