ReactiveX Redux

    Все, кто работает с Redux, рано или поздно сталкиваются с проблемой асинхронных действий. Но современное приложение разработать без них невозможно. Это и http-запросы к бэкенду, и всевозможные таймеры/задержки. Сами создатели Redux говорят однозначно — по умолчанию поддерживается только синхронный data-flow, все асинхронные действия необходимо размещать в middleware.

    Конечно, это слишком многословно и неудобно, поэтому тяжело найти разработчика, который пользуется одними только “нативными” 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.
    • Пишите тесты.
    • Не стреляйте из пушки по воробьям.
    Альфа-Банк
    153,69
    Компания
    Поделиться публикацией

    Комментарии 31

      +6
      Какой кошмар, и это лаконичный и красивый код? Да этож п****ц. Вы делайте ремарки, что для МЕНЯ это красивый и лаконичный код.
        +4
        Я уже начал думать: «Неужели со мной что то не так?»
          0
          Ты не одинок, «мы» существуем и это хоть как-то утешает)
        0

        const fetchEpic = action$ => ...
        Это какое-то соглашение так параметры функции называть? Или ваш личный стиль, первый раз просто вижу такое

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

                Напоследок мое любимое. Реализовать запрос к апи, в случае неудачи сделать не более 5 повторных запросов с задержкой в 2 секунды. Вот что имеем на сагах

                Очень нехорошо вырывать из контекста код redux-saga.js.org/docs/recipes

                На Саге 1 раз пишем хелпер для N количества асинхронных запросов и везде используем. А вы просто берёте готовый хелпер. И это вся разница?
                  0

                  А почему нативно не написать 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
                  }

                  Иногда кажется, что разработчики за модными библиотеками не замечают возможности языка.

                    0

                    (почитал все комментарии)


                    Все претензии сводятся к тому, что приведен недостаточно сложный пример, который легко реализовать без RxJs. И это правда :-) RxJs намного более мощная штука. Советую все же ознакомиться как следует (на egghead.io, например, хорошие курсы), это, конечно, не silver bullet, но очень удобный инструмент.

                      0
                      Хахах, я боюсь представить что получится на более сложном примере. Ага, удобный инструмент для загона проекта в могилу, повидал я уже такие и не один, в том числе и на бэке, схема стандартная, он пилится с RxJs с каждым днем читаемость и поддержка становится невыносимой и противной в геометрической прогрессии и все кто его пилил увольняются и идут на новые проекты и больше не совершают эту ошибку(взять RxJs) и на смену пытаются найти других дурачков, но естественно никто кто-то в здравом уме не соглашается поддерживать такие чудо проекты, а кто соглашается просто не может, или любое элементарное изменение занимает по несколько дней, а то и неделю и отсюда выкает 2 пути, проект загибаться вовсе, проект переписывается с нули, но уже по человечески без него разумеется.
                        0

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


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


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

                          0
                          Скиньте код на 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;
                              }
                          };
                          
                            0

                            А что это вообще делает в компоненте? :-)


                            Если говорить про 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-ами).

                              0
                              Во первых: вы спрятали условия, а суть не в этом, они должны быть снаружи, т.к. это все нигде не переиспользуется и одноразово только для одной страницы.
                              Во вторых: нету обработки ошибок.
                              В третьих: флага isLoaded я тоже не наблюдаю, это все тоже вы запрятали.
                              С прятками у меня бы этот код был которые в 4 раза, но суть не в короткости, а в наглядности, чтобы ты посмотрел и понял что происходит на конкретном примере.

                              Выполните эти условия плиз, чтобы можно было увидеть всю реальную картину.
                              P.S. на redux пофиг вообще, цель чтобы видя изменения в переменных реакт компонент перерендеривался, а не просто показать как вы получили переменные. В моем примере изменяя переменные компонент перерендеривается автоматом из-за MobX.
                                0

                                Это effects, а флаг isLoaded будет в компоненте, да и не особо он там будет нужен (из observable будет ясно).


                                Условия никому не должны быть снаружи, потому что в реальном коде в Effects не будет прямых вызовов API. Будет какой-нибудь FooRepository с методами loadFoo() и loadDetails().


                                В моем примере все перерендерится само, потому что в компоненте будет store.select и async pipe.


                                Объяснить все про angular+ngrx в одном комментарии сложно, оставлю ссылку https://ngrx.io/guide/effects

                                  0
                                  В любом случае, чем это лучше того что я написал?
                                    0

                                    При постановке задачи "сделать локально в одном компоненте" ничем.


                                    Весь смысл раскрывается, когда куча компонентов используют те же данные, любой компонент может отдать команду что-то сделать, и эти "сделать" могут быть довольно сложными (скажем, параллельно работать со своим API и парочкой сторонних — например, в случае со всякими WebRTC это типичная задача, есть свой REST, свои вебсокеты, сторонние вебсокеты, куда угодно прилетает что угодно и вот это все — без стора будут сплошные race conditions).


                                    Разумеется, это не в любом проекте надо, только в относительно сложных. У меня в основном такие :)

                                      0
                                      Так Rx тут не причем, просто насоздавал классов и функций в которых все спрятал и вызывай себе и вся логика выполнится, только эта логика ведь все равно будет описана где-то, поэтому разницы вообще нету, кроме той, что читать Rx код это вырви глаз
                                        0

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


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


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

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

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


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


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

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

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


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

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

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


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


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

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


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

                                0
                                На сомом деле для этого кейса код на промисах с async/await можно сделать гораздо проще и короче, просто надо написать класс обертку внутри которого будет вся логика асинхронных вызовов и если ещё завершились не все, а когда пользователь кликнул чтобы запустить очередную цепочку вызовов, это вызовет инкремент и все предыдущие запросы будут тупо проигнорированы и всегда будет актуально только последнее действие
                        0

                        Что-то вы переделали reducks

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

                        Самое читаемое