Pull to refresh

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 });
    }   
}
Вот именно, но вы что, это же в императивном человеко-понятном и читаемом стиле, но для дрочеров функциональщины на этого смотреть противно.
На мелкий пример смотреть больно, а когда проект разрастается, появляется сотня обсёрвеблов, вот тут я бы посмотрел, как будут прогорать кресла у новых разработчиков, потому что старых это всё достанет и они тупо свалят.
Так это вообще сейчас классический пример развития событий)
Напомните, зачем нужен SPA со сложной бизнес-логикой на клиенте?
Не увидел вообще никаких плюсов в сравнении с Promise и той же Saga. Уменьшили кодовую базу с 10 строк до 5 — и это считается весомым аргументом для использования другого инструмента?

Напоследок мое любимое. Реализовать запрос к апи, в случае неудачи сделать не более 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 с каждым днем читаемость и поддержка становится невыносимой и противной в геометрической прогрессии и все кто его пилил увольняются и идут на новые проекты и больше не совершают эту ошибку(взять RxJs) и на смену пытаются найти других дурачков, но естественно никто кто-то в здравом уме не соглашается поддерживать такие чудо проекты, а кто соглашается просто не может, или любое элементарное изменение занимает по несколько дней, а то и неделю и отсюда выкает 2 пути, проект загибаться вовсе, проект переписывается с нули, но уже по человечески без него разумеется.

У меня противоположный опыт. :)


В описываемой схеме проблема не в RxJs, а в архитектуре. Сделать плохо можно с любым инструментом.


У меня, правда, ngrx, он сам уже в определенном смысле некоторое структурирование навязывает.

Скиньте код на RxJs аналогичный по функциональности этому и который эталонный по вашему с точки зрения красоты, читаемости и т.п.,
Задача:
Условное получение детальной инфы об итеме и инфы для дополнительного блока на какой-то странице. Мой пример для связки 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 код это вырви глаз

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


Вот, например, была такая ситуация: реалтайм-приложение, в котором на лету в ходе конференции можно создавать опросы, которые появляются у всех участников. Автор бэкенда был упертым фанатом REST-а, и там были отдельные CRUD-ы на сам опрос (с заголовком) и на каждый вариант ответа, а у меня-то кнопочка "Применить" на все сразу, и надо все отсинхронизировать с сервером, причем побыстрее (то есть распараллелить все, что можно), прокинуть в вебсокеты и отрисовать локально (ну то есть экшены и стор). С RxJs получилось очень просто и компактно — forkJoin-ы, mergeMap-ы и вот это все, ну плюс withLatestFrom позволяет вообще не морочиться с локальным состоянием (а зачем, если есть глобальное?). Можно ли без Rx написать? Да конечно, можно, только побольше кода будет.


Насчет "вырви глаз" — вопрос привычки. Через месяц это все воспринимается очень естественно :-)

Разве когда кода побольше и он легко читается и воспринимается это хуже, чем когда его поменьше но читаемость и восприятие хуже?

Хуже читается только для новичка. Либо когда на нем пишут неправильно, делая вложенные subscribe-ы и подобную глупость вместо использования надлежащих операторов (но такое code review не пройдет, разумеется).


С опытом (который приходит за 2-3 недели) читается даже лучше.


Тут прямая аналогия с функциями типа map/reduce/filter. Для новичка будет выглядеть сложнее чем вариант с циклом, а для опытного JS-разработчика — наоборот.

Дело не в новичек/не новичек, а в том что, асинхронный лапшекод любят любители функциональщины вот и все) А по поводу map/reduce/filter это не тоже самое, это не асинхронная лапша. Для кого был придуман async/await, как можно такой кайф игнорировать… Не понимаю

Ровно то же самое, только map/reduce/filter — "в пространстве", а rxjs — "во времени".


Асинхронщина в rxjs наружу вообще никак не торчит, если специально этого не сделать.

В любом случае любителям функциональщины это нравится, а остальным (в том числе и мне) не нравится эта лапша и для нас это вырви глаз)

Если бы я был упертым любителем функциональщины, я бы не писал на Angular. :-)


Я не вижу проблем в совмещении ООП и ФП, по крайней мере, у нас получается, все работает, задачи делаются, все довольны.


А на вкус и цвет все фломастеры разные, конечно.

Ну если сделать прямой перевод на Rx, то получится как-то так:
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)
    }
}


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

На сомом деле для этого кейса код на промисах с async/await можно сделать гораздо проще и короче, просто надо написать класс обертку внутри которого будет вся логика асинхронных вызовов и если ещё завершились не все, а когда пользователь кликнул чтобы запустить очередную цепочку вызовов, это вызовет инкремент и все предыдущие запросы будут тупо проигнорированы и всегда будет актуально только последнее действие
Only those users with full accounts are able to leave comments. Log in, please.