Comments 31
const fetchEpic = action$ => ...
Это какое-то соглашение так параметры функции называть? Или ваш личный стиль, первый раз просто вижу такое
async (dispatch) => {
try {
await Delay(2000)
const [respOne, respTwo] = await Promise.all([fetchOne, fetchTwo])
dispatch({ type: 'SUCCESS', respOne, respTwo });
} catch (error) {
dispatch({ type: 'FAILED', error });
}
}
Напоследок мое любимое. Реализовать запрос к апи, в случае неудачи сделать не более 5 повторных запросов с задержкой в 2 секунды. Вот что имеем на сагах
Очень нехорошо вырывать из контекста код redux-saga.js.org/docs/recipes
На Саге 1 раз пишем хелпер для N количества асинхронных запросов и везде используем. А вы просто берёте готовый хелпер. И это вся разница?
А почему нативно не написать retry
, зачем нужно использовать сторонние библиотеки для базовой функциональности, которая пишется один раз на весь проект?
Если хочется настраивать retry
перед каждым запросом:
const delay = ms => new Promise(resolve => setTimeout(resolve, ms))
const asyncRetry = async (asyncFn, { times, delayMs }) => {
const lastTime = times - 1
for (let i = 0, i < times, i++) {
try {
return await asyncFn()
} catch (err) {
if (i === lastTime) throw err
await delay(delayMs)
}
}
}
Если хочется настроить один раз в момент создания fetch
-функций:
const retryable = (times, delayMs, asyncFn) => (...args) =>
asyncRetry(() => asyncFn(...args), { times, delayMs })
const fetchUser = retryable(5, 2000, async (id) => {
// fetch logic
}
Иногда кажется, что разработчики за модными библиотеками не замечают возможности языка.
(почитал все комментарии)
Все претензии сводятся к тому, что приведен недостаточно сложный пример, который легко реализовать без RxJs. И это правда :-) RxJs намного более мощная штука. Советую все же ознакомиться как следует (на egghead.io, например, хорошие курсы), это, конечно, не silver bullet, но очень удобный инструмент.
У меня противоположный опыт. :)
В описываемой схеме проблема не в RxJs, а в архитектуре. Сделать плохо можно с любым инструментом.
У меня, правда, ngrx, он сам уже в определенном смысле некоторое структурирование навязывает.
Задача:
Условное получение детальной инфы об итеме и инфы для дополнительного блока на какой-то странице. Мой пример для связки React + MobX и функция fetchData вызывается в конструкторе компонента.
Вы напишите аналог с соблюдением этой условной бизнес логики для React + RxJs
fetchData = (itemId) => {
try {
this.isFetching = true;
this.item = await apiRequest(`GET /item/${ itemId }`).send();
let reqUrl;
if (item.type === 'some_type1') {
reqUrl = `GET /some/url1`;
}
else if (item.type === 'some_type2') {
reqUrl = `GET /some/url2`;
}
else {
reqUrl = `GET /some/url3`;
}
this.additionalBlockInfo = await apiRequest(reqUrl).send();
}
catch (e) {
this.errorMessage = e.message;
}
finally {
this.isFetching = false;
}
};
А что это вообще делает в компоненте? :-)
Если говорить про RxJs, то вне NgRx мне сложно привести хорошие примеры, я с redux-observable не работал и так прямо сходу код не напишу, а без него точно будет бессмысленная фигня. В NgRx будет эффект, выполняемый по какому-нибудь LoadFoo экшену, в котором, если item и additionalBlock — единая вещь, будет что-то вида
@Effect onLoadFoo$: Observable<Action> = this.actions$.pipe(
ofType<Actions.LoadFoo>(Actions.LOAD_FOO),
switchMap(action => this.api.get(`/item/${action.itemId}`)),
switchMap(item => this.api.get(this.getUrlByItemType(item.type)).pipe(
map(additionalBlockInfo => ({item, additionalBlockInfo}))
),
map(({item, additionalBlockInfo}) => new Actions.FooLoaded(item, additionalBlockInfo))
);
а если две разные, то еще пара action-ов и два эффекта. Еще может получиться, удобнее, что загрузить надо оба сразу, но Loaded экшены разные, тогда закончится все mergeMap-ом.
Ну и еще reducer, но там все ясно и так.
В реальном коде, конечно же, не было бы никаких this.api.get и this.getUrlBy..., а было бы обращение к инстансу класса, который отвечает за работу с Foo через API.
Отдельная прелесть в том, что в компонентах я заселекчу, получу из стора observable на нужный селектор, и async pipe-ом буду с ним работать, никаких дополнительных телодвижений не потребуется.
Но, опять же, это очень простой пример, тут можно как угодно делать (в angular+ngrx просто удобнее придерживаться единого стиля и работать всегда с observable-ами).
Во вторых: нету обработки ошибок.
В третьих: флага isLoaded я тоже не наблюдаю, это все тоже вы запрятали.
С прятками у меня бы этот код был которые в 4 раза, но суть не в короткости, а в наглядности, чтобы ты посмотрел и понял что происходит на конкретном примере.
Выполните эти условия плиз, чтобы можно было увидеть всю реальную картину.
P.S. на redux пофиг вообще, цель чтобы видя изменения в переменных реакт компонент перерендеривался, а не просто показать как вы получили переменные. В моем примере изменяя переменные компонент перерендеривается автоматом из-за MobX.
Это effects, а флаг isLoaded будет в компоненте, да и не особо он там будет нужен (из observable будет ясно).
Условия никому не должны быть снаружи, потому что в реальном коде в Effects не будет прямых вызовов API. Будет какой-нибудь FooRepository с методами loadFoo() и loadDetails().
В моем примере все перерендерится само, потому что в компоненте будет store.select и async pipe.
Объяснить все про angular+ngrx в одном комментарии сложно, оставлю ссылку https://ngrx.io/guide/effects
При постановке задачи "сделать локально в одном компоненте" ничем.
Весь смысл раскрывается, когда куча компонентов используют те же данные, любой компонент может отдать команду что-то сделать, и эти "сделать" могут быть довольно сложными (скажем, параллельно работать со своим API и парочкой сторонних — например, в случае со всякими WebRTC это типичная задача, есть свой REST, свои вебсокеты, сторонние вебсокеты, куда угодно прилетает что угодно и вот это все — без стора будут сплошные race conditions).
Разумеется, это не в любом проекте надо, только в относительно сложных. У меня в основном такие :)
Конечно, можно все сделать без Rx, написав самому. Но когда логика — это по сути пачка операций над потоком, то с Rx писанины намного меньше.
Вот, например, была такая ситуация: реалтайм-приложение, в котором на лету в ходе конференции можно создавать опросы, которые появляются у всех участников. Автор бэкенда был упертым фанатом REST-а, и там были отдельные CRUD-ы на сам опрос (с заголовком) и на каждый вариант ответа, а у меня-то кнопочка "Применить" на все сразу, и надо все отсинхронизировать с сервером, причем побыстрее (то есть распараллелить все, что можно), прокинуть в вебсокеты и отрисовать локально (ну то есть экшены и стор). С RxJs получилось очень просто и компактно — forkJoin-ы, mergeMap-ы и вот это все, ну плюс withLatestFrom позволяет вообще не морочиться с локальным состоянием (а зачем, если есть глобальное?). Можно ли без Rx написать? Да конечно, можно, только побольше кода будет.
Насчет "вырви глаз" — вопрос привычки. Через месяц это все воспринимается очень естественно :-)
Хуже читается только для новичка. Либо когда на нем пишут неправильно, делая вложенные subscribe-ы и подобную глупость вместо использования надлежащих операторов (но такое code review не пройдет, разумеется).
С опытом (который приходит за 2-3 недели) читается даже лучше.
Тут прямая аналогия с функциями типа map/reduce/filter. Для новичка будет выглядеть сложнее чем вариант с циклом, а для опытного JS-разработчика — наоборот.
Ровно то же самое, только map/reduce/filter — "в пространстве", а rxjs — "во времени".
Асинхронщина в rxjs наружу вообще никак не торчит, если специально этого не сделать.
fetchData = (itemId) => {
this.isFetching = true
of(apiRequest(`GET /item/${itemId}`).send()).pipe(
map(item => {
this.item = item;
let reqUrl;
if (item.type === 'some_type1') {
reqUrl = `GET /some/url1`;
}
else if (item.type === 'some_type2') {
reqUrl = `GET /some/url2`;
} else {
reqUrl = `GET /some/url3`;
}
return reqUrl;
}),
switchMap(url => of(apiRequest(url).send())),
finalize(() => this.isFetching = false)
).subscribe({
next: (additionalInfo) => {
this.additionalBlockInfo = additionalInfo;
},
error: (e) => {
this.errorMessage = e.message;
}
})
}
Читаемости не сильно прибавилось. Но если уж брать Rx, то и подход лучше поменять на реактивный.
Порассуждаем о задаче: необходимо выбирать данные о некотором элементе.
Допустим, что элементов у нас может быть несколько и пользователь их может быстро прокликивать, а отобразить данные о элементе нужно только в том случае, если получен весь набор.
Тогда появляются следующие потребности:
- Собирать всю информацию об элементе и не рисовать (не присваивать в атрибуты объекта) ничего до полного получения инфы
- Не нужно никуда присваивать данные если пришел новый запрос на получение элемента
- Не нужно делать запросы на сервер на каждый клик пользователя — он может делать это очень быстро. Просто по приколу
- Ну и хотелось бы иметь единое место, которое отвечает за получение информации
Если принять эти условия, то код на промисах, даже с async/await разрастется.
На Rx это можно решить как-то так:
class SomeClass {
constructor() {
// Создаем поток, для передачи информации об элементе, который нужно получить
this.needToFetchData$ = new Subject();
// Создадим подписку на поток, в которой будет реализована вся логика
this.needToFetchData$.pipe(
// Запретим выбирать данные об элементе, если на него только что уже кликали
skipUntilChanged(),
// Запретим слишком часто кликать на элементы
debounceTime(300),
// При получении нового id элемента, выставим индикатор загрузки
tap(() => this.isFetching = true),
// выполним запрос на сервер
// Важное замечание оператор switchMap переключает поток на новый при получении данных из родительского потока.
// Если уже было переключение и оно не завершилось, то оно отменяется. Цепочка дальше не пойдет.
switchMap((itemId) => of(apiRequest(`GET /item/${itemId}`).send())),
// Обработаем полученный элемент, чтобы понять как получать данные дальше
map(item => {
let reqUrl;
if (item.type === 'some_type1') {
reqUrl = `GET /some/url1`;
}
else if (item.type === 'some_type2') {
reqUrl = `GET /some/url2`;
} else {
reqUrl = `GET /some/url3`;
}
// Вернем собранный объект из элемента и урла, на который отправляем запрос
return { item, reqUrl };
}),
// Снова переключаем поток, чтобы получить дополнительную информацию
switchMap(itemData => of(apiRequest(itemData.reqUrl).send())
.pipe(
// А тут скомпонуем изначальный элемент и дополнительную информацию
map(additionalInfo => {
return { item: itemData.item, additionalInfo: additionalInfo };
})
)
),
// Выключим индикатор загрузки
finalize(() => this.isFetching = false)
).subscribe({
next: (gotData) => {
// присвоим полученные данные в атрибуты.
// До этого места дойдет только последний кликнутый пользователем элемент.
this.item = gotData.item;
this.additionalBlockInfo = gotData.additionalInfo;
},
error: (e) => {
// Выведем ошибку
this.errorMessage = e.message;
}
})
}
fetchData(itemId) {
// Инициируем получение данных об элементе по его id
this.needToFetchData$.next(itemId)
}
}
В этом случае код получается чище, богаче на различные обработки и защиты от дураков.
+ это все можно пилить на функции и компоновать в более сложные потоки.
ReactiveX Redux