Так вышло, что в данный момент я принимаю участие в разработке фронт-энд приложения (React + Redux), делающего множество запросов к REST API каждую минуту, если не секунду.


Мне надоело на каждый запрос писать REQUEST/FAILURE/SUCCESS (далее RFS) экшны, к ним кейсы для редьюсера, всё это обильно поливать тестами (ведь качество превыше всего).


Я написал очередной велосипед.


Велосипед с реактивным двигателем


Существующая проблема


Уже написано множество библиотек для удобной работы с RFS, но они предполагают детальную настройку каждого из трёх экшнов + написание кейсов для редьюсера + тесты. В основном данные библиотеки можно использовать в 100% случаев написания запросов к серверу. Такая гибкость требует написания массы кода для однотипных задач.


В проекте, которым я занимаюсь в данный момент, 90% запросов выполняют предельно простую задачу: сходи на сервер, возьми данных, преобразуй их немного, положи в state, если что-то пошло не так — жалуйся. Написаны тысячи строк тестов, по своей сути являющихся копипастой с микро-изменениями.


К сути


Доколе? Выходные перед монитором, литр кофе, шаверма на обед и ужин, немного контролируемой магии...


не открывай это

вжух


… библиотека готова.


Функция fromTo(from, to, [through]) возьмёт данные где сказано, преобразует как надо и положит в указанное место (любое в вашем state, но только если для редьюсера вы используете библиотеку immutable). Редьюсер самостоятельно (после обёртки его во fromTo.wrapper) поймёт как работать с данными и предсказуемо изменит state в соответствии с RFS экшнами (о которых библиотека позаботится сама). Тестами покрыто всё (если найдёте багов — давите несчадно или откройте тикет).


Краткое описание возможностей


Простейший способ (на нашем проекте это около половины случаев) использования fromTo экшна это:


// запрос к серверу возвращает {"mood": "happy"} , HTTP200

dispatch(fromTo(
  () => axios.get('dogs.io/good-boy'),
  ['dogs', 'goodBoy'],
));

Вот что в это время будет происходить в вашим state:


Начальное состояние


{
  ...otherReducers,
  dogs: Immutable.fromJS({}),
}

REQUEST


{
  ...otherReducers,
  dogs: Immutable.fromJS({
    goodBoy: {
      isRequesting: true,
    },
  }),
}

SUCCESS


{
  ...otherReducers,
  dogs: Immutable.fromJS({
    goodBoy: {
      isRequesting: false,
      data: {
        mood: 'happy',
      },
    },
  }),
}

Ещё для 40% случаев используется объект в качестве аргумента to и добавляется третий (опциональный) аргумент. Востальных случаях действуем по-старинке (fromTo не панацея, а удобный инструмент).


Разбор функции fromTo(from, to, [through])


Функция имеет 3 аргумента.


  1. from. Говорим где взять данные. Функция, что возвращает Promise. Будет вызвана без аргументов.


  2. to. Говорим куда сохранять данные.


    • Если использовать объект в качестве аргумента, то необходимы 3 ключа: { request, failure, success }. Это 3 координаты для данных в вашем state, куда будут сохраняться:
      request: координаты для булевого значения завершен ли вызов from (например [ 'dogs', 'isFetching', 1 ]),
      failure: координаты для данных, что вернулись при reject (например [ 'cats', 'errors', 2 ]),
      success: координаты для данных, что вернулись при resolve (например [ 'robots', 'data', 3 ]).
    • Можно ограничиться списком (например [ 'goodBoy' ]), тогда данный аргумент будет преобразован в объект { request: [ 'goodBoy', 'is Requesting' ], failure: [ 'goodBoy', 'error' ], success: [ 'goodBoy', 'data' ] }

  3. [through]. Говорим (а можем и не говорить и довериться дефолтам), как преобразовать данные, что вернулись в следствие from(). Объект может содержать один или оба из методов:
    • requestAdapter — функция, что получит на вход данные из resolve. Вернувшиеся данные будут сохранены при SUCCESS,
    • errorAdapter — функция, что получит на вход данные из reject. Вернувшиеся данные будут сохранены при FAILURE.

Итог


В данный момент на проекте мы экономим часы нашего времени и пучки нервов благодаря отсутствию необходимости писать тонны однообразного кода. Вместо 90% экшнов, редьюсеров, тестов — вызывается одна функция dispatch(fromTo(...args)). Мы рады.


Конечно, некоторое количество тестов всё же пишется, но в основном это тесты на аргумент through (обязательно) и дань TDD.


В комментариях хотелось бы увидеть мнение коллег по цеху, конструктивная критика более чем приветствуется. Сырцы в гите, библиотека в энпээме, утка в зайце, заяц в сундуке, а тот на дереве.


Хорошего дня.

Комментарии (58)


  1. rumkin
    23.08.2017 02:54
    +3

    Передавать промис аргументом это антипатерн. Это допускается только в отношении функций, которые предназначены для работы с промисами, например Promise.all или Promise.race. Вместо этого достаточно передать dispatch в then. Вот так:


    requestUserById(1)
    .then(actions.userSuccess, actions.userFailure) // приводим результат к виду {type, payload}
    .then(store.dispatch); // Диспатчим


    1. swandir
      23.08.2017 11:53
      +1

      Почему?
      Я так понимаю, вы говорите об этом как антипаттерне в общем.
      Promise — стандартное значение с чётко определённой семантикой. Его можно принимать и возвращать. Ничем не хуже коллбэка, скорее даже лучше.


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


      1. emigrant90 Автор
        23.08.2017 11:57
        +1

        Соглашусь с комментарием swandir'а. Не совсем понимаю причину, по которой это является антипаттерном. Можно увидеть аргументацию?


      1. rumkin
        26.08.2017 20:56

        Потому что Promise это не совсем стандартное значение. Это любое значение или ошибка, т.е. значение+поведение. Функция в данном случае становится слишком могущественной, она получает неограниченное количество значений, а так же берет на себя обработку ошибок. Так же передача аргументом осложняет отслеживание потока иполнения.


  1. vintage
    23.08.2017 09:07
    +3

    Мне надоело на каждый запрос писать REQUEST/FAILURE/SUCCESS (далее RFS) экшны, к ним кейсы для редьюсера, всё это обильно поливать тестами (ведь качество превыше всего).

    По скользкому пути вы идёте. Ещё пара шагов в сторону от копипасты и от редакса совсем ничего не останется.


    1. emigrant90 Автор
      23.08.2017 12:02

      О, никаких шагов в сторону от редакса сделано небыло. Более того, я и сам бы принялся порицать такие начинания. fromTo возвращает стандартную функцию от (dispatch, getState), которую redux-thunk разбивает на отдельные REQUEST/FAILURE/SUCCESS. Просто мы не прописываем экшны явно — они собираются из аргументов (from, to, through).


      А вот к копипасте у меня откровенная нелюбовь.


  1. VladVR
    23.08.2017 09:25
    +1

    Мне надоело на каждый запрос писать… экшны, к ним кейсы для редьюсера
    RFS можно оставить в стороне, вообще любое действие заставляет писать экшены, кейсы и диспатчи, и это заставляет немного менять подход к разработке, «экономить» на тех вещах, на которых не стоило бы. И вот на это у меня есть решение. Скоро будет.
    Immutable.fromJS({})
    Писать лишние экшены надоело, а каждый раз писать Immutable вместо просто {} не надоело? Это же сколько лишнего кода, не несущего смысловой нагрузки.

    Мое мнение — использовать Immutable не стоит. Стейт становится несериализуемым. У redux-dev-tools, расширения для хром, есть отличная фича — экспортировать таймлайн в файл. Чтобы qa, воспроизведя багу, мог отправить этот файл разработчику, а разработчик в свою очередь его заимпортировал. И после сериализации-десериализации, никаких Immutable объектов в стейте уже не будет, будут обычные POJO, а значит поведение может отличаться.
    Я для этой цели, застраховаться от случайных мутаций, написал враппер для рутового редюсера, который делает deep-freeze над полученным от редюсера новым стейтом. То же самое делается со входящим экшеном, чтобы редюсеры экшен не мутировали, а так же экшены дополнительно сериализуются-десериализуются чтобы редюсеры не завязывались на ссылочное равенство нигде. Разумеется это только дебаг-тайм, в релизном билде недопустимо ибо просаживает производительность. Можно дополнительно сериализовать-десериализовать стейт, но рендер как раз таки должен завязываться на ссылочное равенство.


    1. emigrant90 Автор
      23.08.2017 12:12

      immutable не является объектом статьи, но раз уж пошел трэд — вот мои фифтисэнт.


      Immutable действительно пишется дольше, чем {}, однако прелесть его состоит в разнообразии методов для обработки данных. Благодаря функциональной парадигме и продуманности API можно писать очень удобочитаемый код без привлечения сторонних библиотек. А ведь код читают намного чаще, чем пишут.


      По поводу Вашей библиотеки — дайте посмотреть. С удовольствием почерпну качественных знаний, если таковые там есть (в чём не сомневаюсь).


      Говоря о "экспортировать таймлайн в файл" — не пробовал, надо заняться.


      1. AndreyRubankov
        28.08.2017 09:49
        +1

        Immutable действительно пишется дольше, чем {}, однако прелесть его состоит в разнообразии методов для обработки данных. Благодаря функциональной парадигме и продуманности API можно писать очень удобочитаемый код без привлечения сторонних библиотек. А ведь код читают намного чаще, чем пишут.

        А можете пояснить, про какой API из ImmutableJs Вы говорите?

        В моем представлении, весь необходимый функционал уже есть в ES2015: спреды, мапы, редьюсеры, фильтры…

        Дальше просто берем за правило, что никогда не изменяем объекты, который положили в стор и дело с концом. И никаких дополнительных библиотек не нужно для этого.


  1. jakobz
    23.08.2017 11:29
    -1

    А все от того, что Денис Абрамов не скопировал в редакс эффекты из Elm-а. В результате в redux нет стандартного способа эти эффекты выполнять, в результате чего — все придумывают что-то свое сбоку, и никакой инфраструктуры человеческой вокруг не образуется.

    Ну и до кучи из Elm забыли скопировать идею с вложенными action-ами, которая позволяет хоть как-то делать декомпозицию кода.

    В результате вроде как имеем неплохую идею как работать со стейтом. Но при этом имеем популярную, но совершенно ужасную ее реализацию в JS. И, как следствие, два лагеря — тех, что плачут, колятся, но продолжают трахать Redux; и тех, что забили, и вернулись в каменный век с MobX.



    Вообще, пока мы не оставили попыток прикрутить у себя Redux, у меня была такая идея:
    — если редьюсеру надо загрузить какие-то данные с сервера, он кладет в любое место стейта объектик { type: «DEMAND», from: '/api/posts/get', params: { search: 'bla' }, onLoad: { type: 'posts_loaded' } }
    — есть миддлварь, которая мониторит стейт, исполняет новые «деманды», кладет их результаты в стейт, и если надо — диспатчит события про изменения стейта.
    Скажем есть у нас тот же список постов, с поиском и пейджингом. Мы делаем редьюсер, который создает «деманд», и второй редьюсер, который обновляет params.search. Все остальное — загрузку, втыкание результата в стейт, перезагрузку при обновлении params — делает миддлварь.
    Идеологически, это похоже на Relay/Apollo, только для стейта. Ну и вообще оно прямее ложится на идеологию редакса.


    1. raveclassic
      23.08.2017 12:09

      Ну так вы саги изобрели. Только процессы из редьюсера стартуют.


      1. jakobz
        23.08.2017 13:02
        +2

        Саги — это неконтроллируемая императивная хреновина, которая работает вне Redux-а, вытаскивая из него всю логику, по факту превращая Redux в геморойный и многословный способ менять поля в объекте. Т.е. полный архитектурный фейспалм.

        Подход что я предлагаю, позволяет оставить логику внутри редьюсеров, убрав копипасту про доставание данных с сервера.


        1. raveclassic
          23.08.2017 13:32

          По-вашему, сайд-эффекты в редьюсерах — это контролируемая функциональная конфета? Ок.


          1. jakobz
            23.08.2017 15:19

            Сайд-эффекты в редьюсерах — это отличная идея, если правильно сделать — посмотри как это сделано в Elm, откуда Денис слизал Redux. Убрать же из места, где мы пишем логику приложения, возможность сходить на сервер, когд 50% действий этого требуют — идея предельно идиотская. В случае saga, у тебя вся бизнес-логика переезжает в саги, а саги — это лапша асинхронной императивщины. Непонятно зачем тогда тебе стейт держать в редаксе — положи его уже тогда в window.appState, да и меняй прямо из саг.


            1. raveclassic
              23.08.2017 15:52

              В Elm эффекты декларативные и не выполняются в редьюсерах.


              В редьюсерах редакса пишется не логика, а хранение и реакция данных на события в приложении.


              Из редьюсеров вам по определению нельзя никуда "ходить", в том числе и на сервер. Редьюсеры описывают реакцию данных на событие результата похода на сервер (request/success/failure). Сам этот поход описывается в виде процесса, который находится сбоку (в саге).


              Не знаю, откуда в вас столько ненависти к сагам, и чем они вас обидели, но императивной лапшой там и не пахнет, асинхронщины там по определению нет, те же декларативные эффекты и описание "асинхронного" процесса в виде генератора. Вы удивитесь, но генераторы тоже функциональны.


              Стейт в сторе лежит только потому, что там его легко держать в виде композиции и в виде "реакции на действия". Из саг он не меняется, из саг вы выбрасываете новое событие, на которое реагируют нужные редьюсеры.


            1. Druu
              23.08.2017 17:15

              > В случае saga, у тебя вся бизнес-логика переезжает в саги, а саги — это лапша асинхронной императивщины.

              Саги — это _в точности_ эффекты из elm. Разницы две:
              1. Эффекты в elm исполняет сам ранатйм elm в потрохах, а эффекты саг — исполняет саговский middleware.
              2. js, в отличии от elm, благодаря синтаксису генераторов поддерживает синтаксис, аналогичный синтаксису do-нотации (для определенного подмножества монад). Именно по-этому саги выглядят как «императивная лапша» (то есть хорошо и удобно, на самом деле, выглядят). При этом весь код с сагами спокойно можно написать без генераторов вообще (только придется по-другому сделать middleware).


              1. jakobz
                23.08.2017 18:43
                +1

                Разница в том, что эффекты в ELM пишутся в редьюсерах, т.е. весь код пишется единообразно. Плюс, всем разработчикам библиотек вокруг ELM, понятно как эти эффекты делать.

                А с сагами мы получаем два места, где можно писать код — саги, и редьюсеры. Получается шизофрения. Кроме того, saga — не часть redux. В результате если появляется сделать какую-либо библиотеку поверх redux — как у автора поста, например — то у тебя нет никакой возможности сделать это по-человечески.


                1. raveclassic
                  23.08.2017 18:48
                  +1

                  saga — не часть redux

                  Что же это, если не middleware? По вашей логике, redux-thunk, redux-promise и компания — тоже не часть редакса, и, получается, что на "чистом" редаксе вообще ничего не сделать?


                  1. jakobz
                    23.08.2017 20:16

                    На чистом редаксе — вообще ничего не сделать. Да и на нечистом — тоже. Одна боль, кровь, и копипаста.

                    saga — это опция. Хочешь юзай, хочешь — нет. Дальше я хочу написать библиотеку, которая реализует, скажем, реюзабельную логику по ленивой подгрузке и фильтрации списка чего-нибкдь. И у меня два выбора — либо я делаю на saga, исключая из потенциальных пользователей всех, у кого redux-thunk, либо я эту либу не делаю.

                    Но там даже не с эффектов начинаются проблемы. Даже если захотеть сделать набор реюзабельных редьюсеров — без эффектов — приходится либо велосипедить action-creator-factories, либо не делать реюзабельную либу. Потому стандартные механизмы декомпозиции, заменяющие вложенные action-ы в Elm, в redux тоже забыли положить.

                    Судя по отсутствию вменяемых реюзабельных библиотек под redux — большинство выбирают не делать либы вообще. Поэтому под redux вообще ничего с полки нельзя взять с полки. Скажем над той же redux-form, я рыдал — даже для такой типичной задачи, пришлось ломать концепцию redux, в результате получая самую убогую form-handling-библиотеку, из всех мною виданных.

                    Дизайн redux — непродуманный и ущербный. Хотя сама концепция, в целом, имеет право на существование. Хотя на чистых ФП-языках, том же Хаскеле, в таком стиле особо не пишут — это слишком low-level чтобы быть удобным.


                    1. staticlab
                      23.08.2017 23:07

                      Кстати, вы читали про Repatch?


                      1. jakobz
                        24.08.2017 11:23

                        Не видел, спасибо за линку, мысли там здравые. Я как-то в ту же сторону думал, у меня даже gist лежит с похожим эскизом: https://gist.github.com/jakobz/47cfa3c71a676811c3fe261b4327478b


                        //...
                        export const reducers = {
                            resetVal: ({}) => state => ({ ...state, stringVal: '' }),
                            setVal: (stringVal: string) => state => ({ ...state, stringVal })
                        }
                        
                        export var store = createStore(initialState, reducers);
                        
                        store.resetVal({});
                        store.setVal("Test"); // state - иммутабельный, actions - строго-типизированные и сериализуемые.

                        И надо композицию редьюсеров еще глубже продумывать. Например, вместо самодельных и непродуманных вариантов redux/compose, сделать что-нибудь по мотивам линз/профункторов из мира ФП. Мы у себя много что строим на линзах, и этот подход очень круто работает для тех же форм.


                    1. vintage
                      24.08.2017 00:28
                      -1

                      Дальше я хочу написать библиотеку, которая реализует, скажем, реюзабельную логику по ленивой подгрузке и фильтрации списка чего-нибкдь.

                      Пример этой логики из другого мира:


                      item( index : number ) {
                          const page_size = this.page_size()
                          return this.$.$mol_http.resource( this.end_point() ).query({
                              ... this.filters() ,
                              page : Math.floor( index / page_size ) , 
                              page_size : page_size ,
                          }).json()[ index % page_size ]
                      }

                      Вот и вся библиотека.


                    1. raveclassic
                      24.08.2017 01:26
                      +1

                      Мне кажется, что, в силу отсутствия в доках редакса нужных упоминаний, складывается впечатление, что редакс должен заниматься чем-то большим, чем синхронное обновление состояния приложения. Усугубляют положение отсылки в тех же доках к thunk и promise, которые введены для простоты в небольших приложениях. Ну а как следствие, при росте мы огребаем все известные проблемы с асинхронщиной.


                      Саги — напротив, попытка избавить стор от вещей, которыми он не должен заниматься, а именно, асинхронщиной и длинными транзакциями (цепочками экшенов).


                      А по поводу библиотек, так тут та же самая ерунда, библиотеки — это не приложение, и редакс в них ну совсем ни к чему. На самый худой конец можно предоставить отдельный редьюсер с соответствующими селекторами для ручной компоновки.


                      1. jakobz
                        24.08.2017 13:28

                        Я думаю что саги и thunk — это слишком low-level, чтобы действительно избавить приложения от проблем с асинхронщиной.

                        Если подумать — а откуда вообще асинхронщина в приложении? И почему она такая сложная. Если покопать — 90% асинхронного кода — про синхронизацию данных с сервером — получение данных в кеш, и отправку изменений назад. Т.е. чтобы решать проблему с асинхронщиной, надо брать другую абстракцию. Скажем в сообщении, с которого начался этот тред, я предлагал API вроде:
                        — мы декларируем какие данные нам нужны в виде API-запроса с параметрами
                        — какая-то подсистема достает нам эти данные. Но мы договариваемся, что данные будут не сразу, а сколько-то времени будет { isLoading: true, data: null)

                        Таким образом построены те же Relay/Apollo.

                        Можно также смотреть на проблему, как на проблему асинхронной репликации баз данных, и там уже смотреть на концепции типа immutable transaction log (как в Datomic), и прочие всякие CRTD. Но тут уже надо серьезно менять то, как устроен сам сервер.


                        1. Druu
                          24.08.2017 14:51
                          -1

                          > Если подумать — а откуда вообще асинхронщина в приложении? И почему она такая сложная.

                          В сагах нету асинхронного кода. Они полностью синхронные.


                          1. mayorovp
                            24.08.2017 15:22

                            И в чем же принципиальное отличие между этими строками кода?


                            const user = yield call(Api.fetchUser, action.payload.userId);
                            const user = await call(Api.fetchUser, action.payload.userId);


                            1. Druu
                              24.08.2017 15:37

                              В том, что во второй строчке происходит асинхронный вызов, а в первой строчке никакого асинхронного вызова не происходит, происходит вполне синхронный возврат call-объекта. Что с ним там будет дальше происходить — уже от самого кода саги не зависит. Интерпретатор саги может, например, просто посмотреть на него и всунуть обратно заглушку. При этом никто никуда не будет отправлять никаких запросов, а сага прекрасно будет исполняться в соответствии со своей синхронной, декларативной спецификацией.


                              1. mayorovp
                                24.08.2017 16:13

                                Почему вы не рассматриваете возможность наличия заглушки на месте функции call? В этом случае точно так же никакого асинхронного вызова происходить не будет. Но перестанет ли от этого код быть асинхронным?


                                1. Druu
                                  24.08.2017 16:16
                                  -1

                                  > Почему вы не рассматриваете возможность наличия заглушки на месте функции call?

                                  Потому что ф-я call есть в предоставленном коде. То есть, по коду можно сказать, будет там что-то асинхронное, или нет. А по коду с сагой вы никак ничего про асинхронность сказать не можете. В саге синхронный код и асинхронный выглядят абсолютно одинаково — каким он будет в реальности, зависит от интерпретатора. Который по отношению к саге — сущность внешняя.

                                  > Но перестанет ли от этого код быть асинхронным?

                                  В первом случае код _не асинхронный_. Вам никто не гарантирует, что там будет асинхронный вызов, это зависит не от приведенного кода, а от кода интерпретатора, на который данный код никак не ссылается.


                                  1. mayorovp
                                    24.08.2017 16:30

                                    В каком таком "предоставленном коде" есть функция call?


                                    1. Druu
                                      24.08.2017 16:56

                                      > В каком таком «предоставленном коде» есть функция call?

                                      > const user = await _call_(Api.fetchUser, action.payload.userId);


                                      1. mayorovp
                                        24.08.2017 17:10

                                        А это тогда что? Уже и не функция что ли?


                                        const user = yield call(Api.fetchUser, action.payload.userId);


                                        1. Druu
                                          24.08.2017 17:14

                                          Функция, конечно. Обычная, синхронная функция, которая синхронно возвращает обычный объект вроде { type: «CALL», payload: { func: Api.fetchUser, args: [ action.payload.userId ] } }. Кто и что потом будет делать с этим объектом — неизвестно, узнать из саги это никак нельзя.

                                          В случае же с промисом у вас внутри ф-и call будет применение Api.fetchUser к аргументам — с-но, асинхронный вызов промиса.


                                          1. mayorovp
                                            24.08.2017 17:18

                                            В случае с await ф-я call может сделать что угодно, и узнать из саги это точно так же никак нельзя.


                              1. vintage
                                24.08.2017 16:50

                                const user = yield Promise.resolve(1);

                                const user = await Promise.resolve(1);

                                Где вы говорите тут будет асинхронный вызов?


                                1. raveclassic
                                  24.08.2017 16:54

                                  А промисы йилдить нечестно!


                                  Идея в том, что генератор может быть прогнан синхронно, а await — нет, ну вы знаете.


                                  1. mayorovp
                                    24.08.2017 17:16

                                    Вообще-то await тоже можно прогнать синхронно. Если ему, конечно же, не нативный промис передавать.


                                    Вот пример:


                                    var q = await { then: f => f(42) }
                                    console.log(q); // 42, и эта строчка выполняется без приостановки


                                    1. Druu
                                      24.08.2017 17:18

                                      Конечно, можно. Можно вообще реализовать интерпретатор саг в thenable и у вас будут те же самые саги, только вместо yield — await. И код, конечно, будет синхронным.


                                1. Druu
                                  24.08.2017 16:59

                                  У вас другой код. Зачем вы путаете людей?


                                  1. vintage
                                    24.08.2017 17:10

                                    Да пожалуйста:


                                    const user = yield call(Api.fetchUser, action.payload.userId);
                                    const user = await call(Api.fetchUser, action.payload.userId);

                                    const call = ( uri , id )=> Promise.resolve(1);


                                    1. Druu
                                      24.08.2017 17:15
                                      -1

                                      Вы уверены, что Mayorovp предполагал именно такую реализацию для call? Я более чем уверен в обратном. Прекратите путать людей.

                                      Ну и, да, в обоих случаях код синхронный (при таком call). Что дальше? Что вы этим пытаетесь доказать? Как часто вы используете такой call на практике — и зачем?


                          1. jakobz
                            24.08.2017 19:03

                            Ну, main :: IO () — это тоже чистая функция. Не суть как именно записывать асинхронные алгоритмы — они от этого не перестают быть асинхронными. Что в монаде на Хаскеле, что в лапше колбеков в node.js — концептуально будут одинаковые проблемы.

                            И все эти проблемы с асинхронщиной — они от того, что API между сервером и работа с ним — чаще всего делается разными людьми, по какой-нибудь идеологически дырявой системе вроде REST API.

                            Т.е. на бекенде могут десять СУБД годами реплицироваться через transaction log с ACID-гарантиями, за миллисекунды. Потому что там бородатые дяди придумали как это сделать бронебойно, не потеряв ни байтика, ни центика на счетах. А фронтент с бекендом реплицируюется по хардкору в рукопашную, без всякой системы и какой-либо идеологии. Тупо асинхронно-императивно хреначим какие-то куски данных туда-сюда, положив болт на консистентость, race conditions, выставляя на глазок таймауты у кешей.

                            Решать надо проблему, а не ее следствие — лапшу async-ов, нужную для того чтобы через жопу, дедовскими методами, реплицировать кусок БД с сервера в браузер.

                            GraphQL — пожалуй первый более-менее дошедший до масс заход в эту тему, хотя проблем там тоже тьма.


                            1. raveclassic
                              24.08.2017 22:25

                              А вы pouchdb не пробовали?


                            1. Druu
                              25.08.2017 03:20
                              +1

                              > GraphQL — пожалуй первый более-менее дошедший до масс заход в эту тему, хотя проблем там тоже тьма.

                              Чем асинхронный запрос на сервер с GraphQL отличается от такого же запроса на сервер с классическим REST? С точки зрения фронтенда же разницы вообще нет.


                              1. raveclassic
                                25.08.2017 09:19

                                Думаю, имеется в виду именно Apollo, а не голый GraphQL. Описываете, что вам нужно для компонента, и забываете про пачки одинаковых экшенов и флажки а-ля pending.


                1. Druu
                  24.08.2017 03:07

                  > А с сагами мы получаем два места, где можно писать код — саги, и редьюсеры.

                  Верно. Сами редьюсеры можно писать в сагах (и многие так делают). А редакс при наличии саг по факту не нужен, надеюсь, все-таки отвяжут саги от редакса.


    1. staticlab
      23.08.2017 13:15

      А нет желания написать свой "relm"? :)


  1. justboris
    23.08.2017 11:44

    Перед написанием очередной библиотеки неплохо было бы задуматься о том, зачем вообще создавать лишние экшны. В большинстве случаев реально используется только SUCCESS.


    Архитектурная проблема, красиво завернутая в npm-модуль с тестами, по-прежнему остается архитектурной проблемой.


    1. raveclassic
      23.08.2017 12:06

      Вот поддержу. Более того, можно статус данных (pending, success, failure) запихнуть и в пэйлоад, тогда экшен тайп всего один нужен — FETCH_ЧТО_НИБУДЬ.


      1. emigrant90 Автор
        23.08.2017 12:22

        В данный момент в случае удачного запроса диспатчатся 2 экшна:


        1. { type: '@@redux-from-to/some/path/REQUEST', requestTarget, errorTarget }
        2. { type: '@@redux-from-to/some/path/SUCCESS', requestTarget, dataTarget, data }

        В предложенном Вами варианте экшнов тоже будет 2:


        1. { type: 'FETCH_ЧТО_НИБУДЬ', target, status: 'pending' }
        2. { type: 'FETCH_ЧТО_НИБУДЬ', target, status: 'success', data }

        Плюс варианта в этой библиотеки в том, что я могу сохранять данные о RFS куда хочу и как хочу (что часто для нас является критичным).


        В чём выгода Вашего варианта?


        1. raveclassic
          23.08.2017 13:34
          -1

          Эм. Вам же не нравится писать по 3 экшена на один запрос — ну вот вам вариант, как не писать. Или вы экшены диспатчить для изменения состояния не хотите? Ну так вам тогда не редакс нужен.


          1. mayorovp
            23.08.2017 14:41
            +1

            Так он же ни одного экшена не пишет вручную — все три библиотекой генерируются.


    1. emigrant90 Автор
      23.08.2017 12:16

      Такая мысля есть. На данный момент либа писалась исключительно под наш проект, а мы в обязательном порядке используем все 3 экшна. Мысля же состоит в том, чтобы параметр to мог иметь не все { request, failure, success }, а произвольое их число. Тогда выполним только те экшны, для которых указан путь созранения данных. Если фичу кто-то запросит (или сам сделает PR) — фича будет.


      1. justboris
        23.08.2017 13:03
        -1

        А зачем вы используете все экшены? Потому что так было где-то в hello world написано?


        Для того чтобы показать крутящуюся анимацию при загрузке и сообщение после ошибки необязательно мучать Redux store, можно использовать локальное состояние компонента.


        Посмотрите react-loadable. Это компонент для загрузки модулей, то если заменить import() на dispatch(loadData()), то получится неплохо.


        1. emigrant90 Автор
          23.08.2017 13:26

          Уж так повелось. Мы используем данный подход и он нам очень нравится. И все счастливы. И мы каждый день танцуем и пьём ромашковый чай.


          А если серьёзно — все 3 используются для того, чтобы приложение вело себя абсолютно предсказуемо. Мне не надо детально читать код моих коллег если они пишут так же, как написал бы я. Использование общих паттернов сильно упрощает жизнь на проекте.


          Опять же мы не в тёплом мире Python, где есть только один способ сделать работу правильно. У всех свои вкусы. Нам нравится подход с RFS.


  1. swandir
    23.08.2017 12:44

    Можно делать на всю группу однотипных операций один набор экшенов, а то что у вас в from и to перенести в пэйлоад.
    Вместо промиса передавать url и парметры запроса. Вместо пути для каждого типа экшена — имя/идентификатор ресурса, из которого уже всё выводить по однородной схеме.
    Кода будет меньше, и он будет обычным кодом уровня приложения, без дополнительной ответственности, налагаемой на библиотеку. Также должно стать меньше повторения на стороне использования по сравнению с таким хелпером.


    Вообще, мне не очень нравится идея генерируемых идентификаторов экшенов. На мой взгляд они — интерфейс стора, "методы", точки жесткости приложения.
    За экшеном стоит логика, и если она общая, то и реализовать её можно обобщённо, и пусть управляется данными.


    1. emigrant90 Автор
      23.08.2017 12:59

      Напишите API функции и её результат, т.к. явные преимущество данного подхода мне не совсем ясно.


      Есть 2 причины того, что первым аргументом идёт не адрес в вэбе, а функция с промисом:


      1. Я использую axios, параллельном проекте ребята используют fetch, кто-то может вообще вставить туда setTimeout (и не наша забота судить его).
      2. Иногда нужно сделать больше одного запроса параллельно, а сохранять их надо в одном месте. Пример: юзверь у нас собирается путём запросов (отдельно) базовых данных, данных о компании, данных о доверенном лице… всего 6 запросов на разные эндпоинты. В таком случае аргумент from выглядит так: () => Promise.all([ запрос1, запрос2, ... запрос6 ]). Конечно, можно их описать и сгенерировать уже "под капотом", но данный подход показался нам более прагматичным и простым.

      За экшеном стоит логика, и если она общая, то и реализовать её можно обобщённо

      Собственно, этим библиотека и занимается.


      1. swandir
        23.08.2017 13:48

        Я имел ввиду просто action creator и reducer. Это неуниверсальное решение. Чтобы как можно проще.
        Предполагается, что если в конкретном случае логика другая (компановка нескольких запросов или ещё что-то), то тут уже можно подумать об отдельном экшене.


        Кстати, в vuex существует разделение на mutations и actions. Первые всегда синхронны и меняют стейт. Вторые могут быть асинхронны, содержат логику приложения и вызывают первые. Вроде как попытка чётче отделить "логику данных" от "логики поведения".