Pull to refresh

Comments 58

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


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

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


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

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

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

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

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

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


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

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

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

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


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


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


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

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

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

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

Дальше просто берем за правило, что никогда не изменяем объекты, который положили в стор и дело с концом. И никаких дополнительных библиотек не нужно для этого.
А все от того, что Денис Абрамов не скопировал в редакс эффекты из 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, только для стейта. Ну и вообще оно прямее ложится на идеологию редакса.

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

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

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

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

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

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


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


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


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


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

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

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

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

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

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

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

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

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

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

Не видел, спасибо за линку, мысли там здравые. Я как-то в ту же сторону думал, у меня даже 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, сделать что-нибудь по мотивам линз/профункторов из мира ФП. Мы у себя много что строим на линзах, и этот подход очень круто работает для тех же форм.

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

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


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 ]
}

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

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


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


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

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

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

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

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

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

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


const user = yield call(Api.fetchUser, action.payload.userId);
const user = await call(Api.fetchUser, action.payload.userId);
В том, что во второй строчке происходит асинхронный вызов, а в первой строчке никакого асинхронного вызова не происходит, происходит вполне синхронный возврат call-объекта. Что с ним там будет дальше происходить — уже от самого кода саги не зависит. Интерпретатор саги может, например, просто посмотреть на него и всунуть обратно заглушку. При этом никто никуда не будет отправлять никаких запросов, а сага прекрасно будет исполняться в соответствии со своей синхронной, декларативной спецификацией.

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

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

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

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

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

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

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

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

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

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

const user = yield Promise.resolve(1);

const user = await Promise.resolve(1);

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

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


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

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


Вот пример:


var q = await { then: f => f(42) }
console.log(q); // 42, и эта строчка выполняется без приостановки
Конечно, можно. Можно вообще реализовать интерпретатор саг в thenable и у вас будут те же самые саги, только вместо yield — await. И код, конечно, будет синхронным.
У вас другой код. Зачем вы путаете людей?

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


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);
Вы уверены, что Mayorovp предполагал именно такую реализацию для call? Я более чем уверен в обратном. Прекратите путать людей.

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

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

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

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

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

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

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

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

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

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


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

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

В данный момент в случае удачного запроса диспатчатся 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 куда хочу и как хочу (что часто для нас является критичным).


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

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

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

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

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


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


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

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


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


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

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


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

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


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


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

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

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

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


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

Sign up to leave a comment.

Articles