Каждый, кто работает с 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.
Вторая суперсила Observable — это ее операторы.
Оператор — это функция, которая принимает и возвращает Observable, но выполняет некоторые действия с потоком значений.
Ближайшая аналогия — map и filter из javascript (кстати, такие операторы есть в Rx).
Лично для меня самыми полезными операторами оказались 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 можно представить следующей схемой:
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? Для этого есть замечательная библиотека, наблюдаемая при сокращении.
Его архитектура выглядит так:
Все 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))
Если суммировать плюсы и минусы саги, то получится следующая картина:
Саги просты в освоении и очень популярны, поэтому в сообществе можно найти рецепты практически на все случаи жизни.
К сожалению, императивный стиль затрудняет по-настоящему гибкое использование саг.
Совершенно иная ситуация с Rx:
Может показаться, что Rx — это волшебный молот и серебряная пуля.
К сожалению, это не так.
Порог входа в Rx заметно выше, поэтому сложнее ввести нового человека в проект, который активно использует Rx. Кроме того, при работе с Observables особенно важно быть осторожным и всегда хорошо понимать, что происходит. В противном случае вы можете наткнуться на неочевидные ошибки или неопределенное поведение.
action$
.
ofType('DELETE') .
switchMap(() => Observable .
fromPromise(deleteRequest) .
map(() => ({ type: 'DELETE_SUCCESS'})))
Однажды я написал эпик, который выполнял довольно простую работу — каждое действие типа «DELETE» вызывало метод API, который удалял элемент. Однако во время тестирования возникли проблемы.
Тестировщик жаловался на странное поведение — иногда при нажатии кнопки удаления ничего не происходило.
Оказалось, что оператор switchMap поддерживает одновременное выполнение только одного Observable, своего рода защита от состояния гонки.
В итоге дам несколько рекомендаций, которым следую сам и призываю следовать всем, кто начинает работать с Rx:
- Будь осторожен.
- Изучите документацию.
- Проверьте в песочнице.
- Пишите тесты.
- Не стреляйте воробьев из пушки.
-
Лазерный Принтер Dell 1710N
19 Oct, 24 -
2Гис – Гостиничный Эксперт
19 Oct, 24 -
Выпущена «Казуальная» Версия Xbox 360
19 Oct, 24