Есть рекомендации, но и их не все читают.
Очень многие вообще используют Redux исключительно потому что «все так делают», что зачастую сводит его полезность к нулю, или вообще просто бессмысленно усложняет приложение и добавляет ему лишних ошибок.
Возможно, то что я здесь пишу, покажется для очень многих очевидным, но лично для меня таковым это не было — до всего пришлось доходить изрядно потоптавшись по граблям. И, как я сейчас замечаю, не только для меня: в последние полгода мне пришлось доделывать или модифицировать несколько начатых другими людьми проектов на React и ReactNative, которые использовали Redux.
И, так как заботливо разложенные грабли, которые пришлось вычищать из чужого кода, были практически те же самые, мне захотелось рассказать об этом.
Давайте рассмотрим несколько примеров, я их специально брал из разных проектов, они написаны разными людьми из разных стран.
Синхронный action:
export const DISABLE_WELCOME = 'DISABLE_WELCOME';
export function disableWelcome() {
return {
type: DISABLE_WELCOME,
payload: {}
}
}
и reducer для него:
export default function (state = INITIAL_STATE, action) {
switch (action.type) {
// много разных других reducer'ов
case DISABLE_WELCOME:
return {
...state, auth: {
loggedIn: true,
errorMessage: null,
welcome: false
}
};
default:
return state;
}
}
В общем, всё просто и очевидно, никаких подводных камней особо нет.
Теперь посмотрим на асинхронный вызов из того же проекта, в административной части в таблицу добавляется новый вид некой «подписки»:
export function createSubscription(props) { // Create Subscription
const request = axios.post(`${ROOT_URL}/subscription`, props,
{
headers: { Authorization: getAuthToken() }
});
return {
type: CREATE_SUBSCRIPTION,
payload: request
}
}
и простенький reducer
case CREATE_SUBSCRIPTION:
return {...state, subscription:action.payload};
Всё сделано совершенно по инструкции. В качестве action payload передаётся promise, при инициализации redux'a в него добавляется библиотечное middleware redux-promise, reducer вызывается после получения ответа от сервера, вроде как всё нормально.
Но сразу бросаются в глаза две проблемы:
- Reducer вызывается только после получения ответа от сервера, а хорошо бы звать его два раза, в начале запроса и в конце (например чтобы сразу добавить в таблицу значение, которое создал пользователь, а не ждать ответа от сервера и только после этого обновлять UI — это чисто внешне выглядит значительно лучше)
- Те же самые строки с функцией getAuthToken() повторяются в том же файле actions.js около 40 раз.
В данном проекте первая проблема была решена достаточно брутальным образом, программист просто вызывал два action подряд, один с простым объектом для обновления UI, второй с промизом.
Варварство? Варварство.
Работает? Работает.
Более правильный и чаще встречающийся подход предполагает использование асинхронного middleware которое бы вызывало бы reducer два раза, при вызове action и после получения ответа:
export default function promiseMiddleware({ getState }) {
return next => action => {
// проверяем если наш action payload это Promise, то
if (isPromise(action.payload)) {
const { type, payload, meta = {} } = action;
// сразу зовём reducer с meta.sequence: 'begin'
next({
...action,
payload,
meta: {
...meta,
sequence: 'begin'
}});
payload.then(
// по завершению reducer с meta.sequence: 'complete'
result => next({
...action,
payload: result,
meta: {
...meta,
sequence: 'complete'
}}
))
// и если была ошибка, то reducer с meta.sequence: 'error'
.catch(err => next({
...action,
payload: err,
meta: {
...action.meta,
sequence: 'error'
}}
));
} else {
next(action);
}
};
}
То есть что мы здесь получаем: reducer вызывается два раза, один раз вначале (meta.sequence === 'begin') и в конце при успешном завершении запроса (sequence: 'complete') или при ошибке (sequence: 'error').
Пару раз я встречался с выносом сюда же функций для управления аутентификацией, что-то вроде такого:
const token = getState().getIn(['session', 'token']);
if (token && typeof action.payload.set !== 'undefined') {
action.payload.set('Authorization', token);
};
Понятно, что выглядит не очень аппетитно — привязано к конкретному месту хранения токена в конкретном store (в данном случае это ещё и Immutable), привязано к конкретной библиотеке для http запросов, если в качестве action payload будет передана какая-нибудь другая функция с методом или свойством set (а название, признаемся, не самое редко встречающееся), последствия могут быть самыми непредсказуемыми, ну и так далее. Однако, тоже работает.
И, к сожалению, стандартные и везде описанные возможности для работы с асинхронными вызовами на этом заканчиваются. Дальше начинается кто во что горазд.
Первая проблема возникает, когда нужно скомбинировать несколько асинхронных функций, причём нужно чтобы они вызывали разные reducer'ы.
Другая проблема, когда у функции не один callback, а несколько. Всё, промизом здесь уже не обойдёшься. В результате я вижу dispatch, передающийся в функцию createAction чтобы создавать при поступлении нового callback'a ещё больше action'ов. Или вижу как весь кусок с websocket'ами, например, переезжает в какой-нибудь реактовский компонент из action'ов, в результате чего впоследствии несколько дней тратится на то чтобы понять, почему вдруг всё иногда перестаёт работать (а всего лишь компонент решил перемонтироваться).
Я сейчас покажу вам пару реальных примеров реального кода, и постараемся забыть об этом как о страшном сне.
const disconnect = createAction('DISCONNECT_MESSENGER',
() => {
client.unsubscribe('/messenger')
client.disconnect();
});
const wasDisconnected = createAction('WAS_DISCONNECTED_MESSENGER',
() => client.unsubscribe('/messenger'));
const receive = createAction('RECEIVE_MESSAGE');
const connect = createAction('CONNECT_MESSENGER',
( token, dispatch ) => client.connectAsync({ auth: { headers: { Authorization: token } } })
.then(() => {
client.onDisconnect((willReconnect, log ) => dispatch(wasDisconnected({willReconnect, log})))
return client.subscribeAsync('/messenger', message => dispatch(receive(message)))
}),
( payload ) => payload );
dispatch(editStory(formData, id)).then((results) => {
if (results.payload.data.error === true) {
alert.error('Error', results.payload.data.message)
}
else if (results.payload.data.error === false) {
this.setState({ loadingClassName: '' });
this.close();
this.props.fetchStory();
dispatch(push('/story'));
alert.success('Success', results.payload.data.message)
}
}).catch((err) => {
dispatch(push('/story'));
alert.error('Error', 'Oooops! Looks like something went wrong. Please try again after sometime.');
})
Да, .then() от dispatch(). Оно работает, кстати…
Да, есть варианты использовать что-нибудь для создания последовательности action'ов или Thunk, но почему-то в реальности каждый раз я вижу именно такое.
Что делать
Отправляем всё в middleware. Целиком.
Action — это действие. Действие — это отправить данные на сервер или получить данные с сервера, а не «создать promise» — promise, как и библиотека для асинхронных запросов, будь это хоть fetch, хоть axios, хоть superagent — это всё внутренняя кухня, давайте уберём её из логики:
const requestMiddleware =
({ getToken } = {}) => // Давайте передадим тут функцию для получения токена, всего один раз
({ getState }) => // И у нас здесь есть полный доступ к state!
next =>
action => {
// обозначим то, что мы хотим обратиться именно к нашему middleware
// с указав в payload use: 'request'. Можно придумать что-нибудь получше
if (action.payload && action.payload.use === 'request') {
const {
payload,
type,
meta = {}
} = action;
const {
url,
method,
data,
query
} = payload;
// И нам без разницы какая библиотека этот "request"!
// Заменим здесь - заменится для всех запросов.
const myRequest = request(method, url);
if (typeof getToken === 'function' ) {
// токен прикладываем тоже здесь. Так как нам удобно.
// А для его получения используем внешнюю функцию, чтоб особо не хардкодить
myRequest.set('Authorization', getToken(getState()));
}
// Выставляем всё что надо
if (query) {
myRequest.query(query);
}
if (data) {
requestObject.send(data);
}
myRequest.then(response => next({ // Зовём reducer если всё успешно
...action,
payload: response.body,
meta: {
...meta,
sequence: 'complete'
}
})
).catch(err => next({ // Зовём reducer если всё плохо
...action,
payload: err,
meta: {
...action.meta,
sequence: 'error'
}}
));
next({ // Зовём reducer в самом начале
...action,
meta: {
...meta,
sequence: 'begin'
}});
} else {
next(action); // Не забываем об остальных
}
}
Теперь добавим наш middleware:
const options = {
// наша функция для получения токена
getToken = store => store.getIn(['session', 'token'])
}
const store = applyMiddleware(
requestMiddleware(options)
)(createStore)(reducer);
И всё. Можно закрывать этот файл навсегда и пользоваться:
const myAction = ({
type: 'GET_REMOTE_URL',
payload: {
use: 'request',
method: 'GET',
url: 'https://your.site/api'
}
});
Не сомневаюсь, что всё можно сделать и изящнее, и удобнее.
Но это самая простая часть, а если нам надо добавить ещё какие-то callback'и? Например, мы загружаем длинный файл и нам нужно обновлять store в зависимости от прогресса загрузки?
Да не вопрос — если наша библиотека для запросов позволяет добавить, например, event on('progress') просто пишем:
const handleProgress = progress => next({
...action,
request,
payload: {
progress: progress.percent
},
meta: {
...action.meta,
sequence: 'progress'
}
})
myRequest.on('progress', progress => handleProgress(progress))
Всё. Готово. Проверяйте в своём reducer'e sequence === 'progress'
Нужно добавить socket.io или что-нибудь подобное? Пожалуйста, точно так же.
Создайте, на все события вызывайте next() с нужными вам параметрами, и всё.
И точно так же можно поступать с абсолютно любыми асинхронными функциями, которые вы вызываете более одного раза (всё-таки если один раз — смысла особого нет).
Очень удобно сделать отдельный middleware для задержек и таймеров — всё-таки всякие setInterval() в аккуратном коде аккуратного компонента смотрятся довольно чужеродно.
В ReactNative вы можете отправить туда Alert или вызов какого-нибудь нативного компонента типа react-native-image-picker — и если вдруг вам срочно придётся заменять его на какой-нибудь react-native-image-crop-picker (а подобные необходимости обычно возникают чуть чаще, чем хотелось бы...) — вы замените его всего в одном месте, а не в десятке.
Сделайте разные middleware для LocalStorage для веб-страницы и для AsyncStorage в ReactNative, но сделайте способ обращения к ним одинаковым — и вы сможете использовать одни и те же action и для сайта и для приложения.
В общем, в action оставляем только логику. Чем меньше там конкретной реализации — тем лучше. Оставим всю грязную работу для middleware.
Я собрал несколько своих middleware, которыми я пользуюсь постоянно, в библиотечку Redux Kittens, может быть кому-нибудь тоже пригодится.
Комментарии (39)
parakhod Автор
10.10.2017 17:44+2Если у меня в коде стоит два диспатча рядом, я начинаю подозревать что я что-то делаю не так. Если во всём коде эти два диспатча ходят исключительно вместе, то подозрение перерастает в твёрдую уверенность.
Что же касаемо частого рендера, то в реакте это за грех не считается. Да и рендер-то не настоящий же. Ну а на крайний случай всегда shouldComponentUpdate имеется.vasIvas
10.10.2017 17:59-2Безусловно могут быть ситуации, в которых два диспатча, это следствие непродуманной архитектуры построения логики. Но я такие моменты даже не рассматриваю. Я, как пример, могу привести запись в хранилище и в localStorage.Данные одни, а редюсеры, как минимум два. В такой ситуации и нужно делать два диспатча. Не смешивать же редюсеры в кучу? И какой-бы быстрый рендер не был, он все равно будет затратней чем два диспатча в десятки, если не сотни раз. нужно уметь выявлять узкие места и думать только о них.
parakhod Автор
10.10.2017 18:58Зачем два диспатча, если вы пишите одно и то же? localStorage вообще синхронный, от AsyncStorage при записи тоже никакой асинхронности обычно не требуется — разместите функцию записи хоть в функции создания action, хоть в редюсере (я предпочитаю второй вариант) — и всё прекрасно будет работать с одним dispatch.
А экшны имеет смысл упрощать не для ускорения — какое тут ускорение — а для уменьшения количества возможных логических ошибок.vasIvas
10.10.2017 19:25В actionCreator нельзя, нужно в reducer. И когда я говорю о диспатче, то подразумеваю диспатч из actionCreator. То есть, когда Вы данные из компонентов получили, затем их обработали, а вот уже потом диспатчите. Если данные одни и те же, как я привел в неудачный пример, то да, можно обойтись одним. Если же, допустим в localStorage, нужно часть данных записать, то это уже два диспатча, так как лишнее передавать не правильно. И я не говорю что экшены нужно ускорять, я как раз утверждаю обратное.
parakhod Автор
10.10.2017 19:36«В actionCreator нельзя» — почему нельзя-то? Не очень изящно, но работать будет.
vasIvas
10.10.2017 19:48+1Потому что, это «Варварство». redux это слой между хранилищами-сервисами и реактом. Даже запросы в actionCreator делать неправильно. Логику нужно реализовывать «где-то», а инициализировать уже в actionCreator. А с хранилищами, к каковым относится и localStorage, нужно работать с помощью reducer.
К тому же я могу расширить пример ситуаций, когда при получении данных от представления, нужно не просто их сохранить, но и на их основе получить результат выражения и его сохранить в localStorage. Тут на лицо один actionCreator, который вызывает два других. Только вопрос, где именно будет вызывать dispatch, немного может не уложится в мой первый неудачный пример. потому что, dispatch может вызываться в двух других actionCreator, но при этом, это все равно будет два диспатча подряд.
И ещё стоит добавить, что если вы сделаете правильно и вынесите запросы из actionCreator, то не придется сорок раз токен получать. actionCreator вообще не должны знать о токенах.parakhod Автор
10.10.2017 20:40И ещё стоит добавить, что если вы сделаете правильно и вынесите запросы из actionCreator, то не придется сорок раз токен получать. actionCreator вообще не должны знать о токенах.
Собственно у меня вся заметка как раз об этом.
Что же касается «делать неправильно» — я тоже обычно придерживаюсь такого подхода, однако когда у меня возникает дилемма написать что-то прямолинейно и «неправильно» в две строчки или «правильно», но в пятьдесят, при том что делать это будет одно и то же и никаких подводных камней не предвидится, интуиция, опыт и лень подсказывают мне использовать первый вариант.
Тот же storage, возьмём для примера AsyncStorage из реакт-натива — он асинхронный, и он уже готовый промиз — я могу городить огород пятнадцатью различными «правильными» способами, а могу кинуть AsyncStorage.get() в прямо payload экшна, а всякие AsyncStorage.set() останутся в редюсерах. Пусть это триста раз будет неправильно с чьей-то точки зрения, но я не пложу лишнего кода только ради «правильности» и тем самым в реальности уменьшаю вероятность возникновения ошибок.vasIvas
10.10.2017 20:49AsyncStorage.get() в прямо payload экшна, а всякие AsyncStorage.set() останутся в редюсерах
Более правильно сделать в actionCreator, так как все с чем Вы работаете с помощью reducer, хранится в состоянии приложения. А как известно в actionCreator помимо dispatch идет ещё и state. То есть, в некоторых случаях, даже обращение к хранилищу не требуется.
ElianL
11.10.2017 15:19reducer'ы должы быть чистыми функциями, нельзя в них писать что-то в localStorage.
parakhod Автор
11.10.2017 16:51-1Почему нельзя?
Это противоречит заповедям Моисея, или правилам пожарной безопасности?
В чём смысл расовой чистоты функции редюсера если она в любом случае возвращает новый state?
Какой смысл мне городить listener'ы над этим state, или ещё хуже в реакте отслеживать componentWillReceiveProps, всего лишь чтобы сделать какое-то простое действие к стейту не относящееся, но происходящее исключительно в тот же самый момент если он обновляется определённым образом?
А если я не запись в localStorage сделаю, если я какой-нибудь alert оттуда покажу, что, меня нужно разжаловать из программистов в филологи?faiwer
11.10.2017 20:28Всякий раз делая шаг в сторону, важно понимать его последствия. Некоторые side-effect-ы могут сильно усложнить жизнь в будущем, когда всё почему-то очень сильно запутается и станет работать не просто и прямолинейно, а чёрт знает как, хотя вроде бы… ой. А это обязательно случится с течением времени. Вопрос лишь в сроках.
По сути, имея столько ограничений, сколько накладывает на наши плечи связка redux+react, при этом при достаточно сложных задачах (большие и сложные приложения), нам так или иначе приходится делать что-то "противоречащее заповедям". И если мы понимаем для чего это, понимаем, что в данном случае это разумное и обоснованное решение, которое даёт такие преимущества, что недостатками можно пренебречь — то ок. Мы делаем это со знанием дела.
А если это просто, потому, что лень. То туды её в качель. Скажем если возникает необходимость писать что-то в localStorage из редьюсеров в разных местах… То может сделать 1 .substribe и некий manager-обёртку, которая позволит всё это дело систематизировать и не переусложнять элегантную простую схему, заложив мину туда, где её никто не ждал...
Так, мысли в слух.
parakhod Автор
11.10.2017 22:12+1Здравый смысл, здравый смысл и ещё раз здравый смысл.
Я получаю токен с сервера и сохраняю его в storage, один раз в одном месте — да, в редюсере, да мне так нравится.
Считываю я его тоже один раз в одном месте, при инициализации — ну в чем тут даже теоретически может возникнуть проблема?
Требование неукоснительно соблюдать правила ради самих правил немножко смахивает на армию и, как и в армии, кроме зашкаливающего идиотизма обычно ни к чему не приводит.
Nerlin
11.10.2017 22:01Потому что это нарушает SRP.
Почему в таком случае не выделить функцию, которая будет запускать этот необходимый код, а затем вызывать dispatch и изменять состояние приложения? Зачем мешать логику изменения текущего состояния с кешированием или другими процессами?parakhod Автор
11.10.2017 22:29-2Когда вы в следующий раз будете сидеть и программировать, и вам вдруг захочется в туалет, ни в коем случаев не ходите туда — это нарушит srp. Пусть туда за вас сходит кто-нибудь другой, а вы программируйте, не отвлекайтесь.
Ну серьёзно, в чем смысл каждую концепцию доводить до абсурда? Тем более что эти концепции склонны проходить путь вначале от рекомендаций до догм, а потом до устаревших и нерекомендуемых к использованию в среднем лет за 20?
Предложенный вами подход в приложении к простейшей задаче отправки запроса на сервер, получения токена и сохранения его одновременно в state и storage увеличивает количество необходимого кода в разы. А чем больше кода, тем больше ошибок.
Всё-таки принцип бритвы Оккама гораздо старше, и вполне доказал свою состоятельность.
Nerlin
12.10.2017 00:35+1До абсурда доводите Вы, приводя такие примеры. Более верной аналогией в Вашем же примере было бы: «Хотите в туалет — не срите там же, где работаете». Почему в квартире есть отдельные места для каждого занятия — зал, кухня, спальня, туалет, а не одна комната для всего? Возможно, Вам было бы удобно одновременно стоять в душе и готовить яичницу, но это не значит, что это будет действительно эффективно.
Ближе к делу. Кода не увеличится в разы, добавится одна лишь функция, занимающая строки две, по сравнению с тем же кодом, который расположен будет у Вас в reducer с нарушением SRP, зато reducer при этом останется чистым. Изменится лишь вызов. К тому же, Вы рассматриваете лишь тот случай, когда сохранять одновременно нужно и в store, и в storage, а если нужно что-то сохранить лишь в store с тем же action или если нужно применить sessionStorage или вообще что-то иное почему-то нет. Может конечно у Вас весь store всегда хранится в localStorage, я не знаю, но смысла в этом я не вижу никакого, зачем эта зависимость в принципе нужна в reducer'е? Человек, не знакомый сразу с Вашим проектом, должен будет изучить эту особенность реализации и знать, что что-то изменяя в state, он вносит изменения и в localStorage, лучше эту особенность выделить явно в отдельную функцию.parakhod Автор
12.10.2017 10:24Да, я привожу абсурдный пример с бытовами аналогиями чтобы показать абсурдность догматичного восприятия каких-то принципов. Принципы тоже живые люди придумали, и я себя, лично, не считаю сильно глупее их.
Теперь попрошу вас внимательно перечитать то, что я написал о конкретно моей задаче (не такая уж редкая задача, кстати).
В storage сохраняется только токен (иногда ещё информация о текущем пользователе).
Данные эти приходят после выполнения определённого (и единственного) запроса к api.
Запрос делается с помощью middleware, которое собственно и выполняет запрос, и вызывает редюсер два раза — вначале и по окончанию.
Итак, внимание вопрос, где по-вашему должна «добавиться ещё одна функция занимающая строки две»?
Нажимается кнопка «login» — там никакого токена нет.
Диспатчится экшн который вызывает middleware для запроса api — там тоже токена нет.
Токен я в первый раз увижу в middleware по завершении запроса — но middleware не особо должно интересовать что за данные оно получило, оно не по данным — оно за запрос отвечает.
А потом всё, редюсер. И первый и последний раз в чистом виде я токен вижу там.
Я могу получить его и сохранить и в state и забекапить в storage (да, кстати у меня это делается не то что отдельной функцией, а отдельной библиотекой — она поддерживает и localStorage и реакт-нативовский AsyncStorage — не вижу смысла писать почти одинаковый код два раза для разных платформ.)
Так что если не хватать его там — единственный способ как это ещё сделать — это вешать listener который будет следить за изменением токена в state и сохранять его в storage.
И зачем? Он будет проверять каждое шеаеление стейта. Если какой-то другой редюсер по ошибке испоганит стейт он честно запишет в storage новый мусор.
Тормоза и потенциальные источники ошибок исключительно ради фанатичного соблюдения принципа которому лет в три раза меньше чем я программированием занимаюсь?
Извините, нафиг.staticlab
12.10.2017 11:18Сегодня надо сохранить в LocalStorage, завтра ещё и на сервер. При этом не забыть отправить нотификации об успехе или ошибке. Подобные операции в редьюсере делать уже нельзя, надо рефакторить. Кстати в redux-saga для подобных операций на экшен навешивается сага-обработчик, которая сама по себе позволяет делать асинхронные операции, вызывать внешние API и порождать новые экшены.
parakhod Автор
12.10.2017 11:33«Сегодня надо A, завтра ещё и B» — в своё время такой подход к программированию назывался «позолотой». Придумывание теоретической возможности необходимости использования дополнительного функционала в будущем для оправдания ненужного усложнения кода сейчас.
Макконнел сильно осуждал.staticlab
12.10.2017 11:37Но вы же своё решение предлагаете использовать другим, не так ли? Тем более, что сильного усложнения ведь и не предлагает никто. А упрямое упрощение вот прямо здесь и сейчас, кажется, ничуть не лучше неоправданного усложнения.
parakhod Автор
12.10.2017 11:46Я никому ничего не предлагаю.
Я рассказываю про middleware и свой опыт с ним.
Вы знаете, я давно вышел из того возраста когда мне были необходимы признание и обратная связь — если кто-то захочет воспользоваться моим опытом или чем-то что я выложил в открытый доступ — пожалуйста, берите, не жалко.
Кто-то может мне аргументированно возразить — тоже, выслушаю с интересом. Учиться надо постоянно и я, если надо, признаю свою неправоту даже с благодарностью — это в первую очередь мне самому будет полезно.
Однако когда вся аргументация сводится к «потому что так не положено», а не положено потому что какой-то авторитет когда-то так сказал — ну извините, это не аргумент.
Призыв к «чистоте функций» так же в абсолютности своей нелеп как и призыв к отказу от глобальных переменных, от goto, к отказу от функций с полным переходом на методы классов, отказу от короткого наименования переменных, обязательному использованию нотации считаемой очередным гуру единственно правильной, обязательной типизации переменных, отказу от обязательной типизации переменных и т.п. — много я уже этого видел, извините.staticlab
12.10.2017 11:48Хорошо, но вы же сами только что аргументировали Макконнеллом и ранее Оккамом...
parakhod Автор
12.10.2017 11:54«Знать» и «слепо использовать» — разные вещи.
Если я сам не могу переформулировать простым языком идею высказанную каким-то авторитетом и не обосновал в своё время сам для себя её правильность — тут уж извините, значит либо эта идея до меня ещё не дошла, либо это не столь хорошая идея.
И опять же — всё это для меня не догма. Я всегда стараюсь руководствоваться исключительно здравым смыслом.
sainttechnik
12.10.2017 09:47Почему бы не использовать еще один middleware, который будет заниматься синхронизацией нужного куска стора с LocalStorege? Вся работа по синхронизации вынесена «куда-то». Не нужно писать два dispatch, не нужно писать сайд эффекты в редюсере, не нужно писать логику сохранения в action.
parakhod Автор
12.10.2017 11:37В принципе идея неплохая, однако она всё-таки тоже немного отдаёт усложнением. А где усложнение — там всегда ошибки и глюки.
staticlab
10.10.2017 17:48+2Кажется, это те же эффекты из redux-saga, только вид сбоку.
parakhod Автор
10.10.2017 19:05Глянул краем глаза — ну да, суть та же. Правда на мой вкус это излишнее преумножение сущностей, если всё можно сделать и стандартными средствами, а особой выгоды ни в размере кода ни в его читаемости я не вижу.
В общем, мой внутренний Оккам не одобряэ ))staticlab
10.10.2017 19:25Суть redux-saga с одной стороны — замена прямого вызова асинхронных операций вызовом эффектов (т.е. аналогично вашим миддлварам), а с другой стороны — использование генераторов, позволяющих компоновать асинхронные операции аналогично async/await, но с возможностью пошагового тестирования.
raveclassic
12.10.2017 01:19Ну где ж стандартными, у вас самопальная мидлварь.
К слову, ваше решение поддерживает отменяемость и рейсы? А тестируется так же удобно как саги?parakhod Автор
12.10.2017 10:01Миддлварь — стандартный подход, ну а моё отношение к преумножению сущностей я уже высказал.
Что же касается самопальности — ну да, я сделал то что надо именно мне и сделал под себя — «универсальные» решения чаще всего страдают от того, что они призваны удовлетворять всех, и поэтому полностью не могут удовлетворить никого.
Отменяемость поддерживает, что такое рейсы не знаю.
В тестировании именно как тестировании в отношении фронтенда или мобильных приложений у меня необходимости никогда не возникало, это ж не api. У меня достаточно удобно настраиваемый консольный вывод дебажной информации — мне этого достаточно.
Опять же — все свои библиотеки я пишу исключительно для себя лично и до последнего времени я их даже на гитхаб не выкладывал. Это решение удобное лично для меня — а в этой заметке я вообще пишу даже не о решении, а о подходе к нему. И у меня нет ни малейших сомнений в том что процентов 80 тех, кто пользуется редаксом, о возможности такого подхода даже и не задумывалось.
7iomka
11.10.2017 08:05Советую посмотреть в сторону redux-logic. Автор проникнут Вашей болью, и рассмотрел все минусы подходов, используемых разными либами, и выбрал на мой взгляд золотую середину)
Fedcomp
11.10.2017 11:42Подобие этого middleware был еще в redux real-world example (в 2015 году точно). Что забавно, поначалу у нас было только начало/конец/эррор (только полноценные события вроде BEGIN_USER_LOAD, END_USER_LOAD, USER_LOAD_ERRROR вместо meta значения), потом тоже пришлось прилепить коллбэки (saga не смотрел).
parakhod Автор
11.10.2017 12:03Ну откровенно нового здесь точно ничего нет, это правда.
И подходов миллион, выбирай какой хочешь.
Лично мне ближе вариант, когда мне приходится меньше писать — я крайне ленив.
То есть я в своё время написал пару утилиток и для экшенов и для редюсеров — я давно не указываю имена редюсеров типа вот этих USER_LOAD, если я обращаюсь к экшн userLoad то у меня зовётся редюсер по имени USER_LOAD, но я его и в сторе не указываю явно, я там пишу что-то типа:
createReducers({ userLoad: { begin: state => state.set('userLoading', true), end: state => state.set('userLoading', false).set('userLoaded', true), error: { 401: state => state.set('error', 'authError'), default: state => state.set('error', 'someGeneralError') } } })
и утилитки всё распихивают за меня, переваривая правильные type и meta.
Соответственно и история с миддлверами способствует тому же — написал один раз, и забыл. А в экшнах остаются аккуратные ровные строчки с именами экшнов, методами и url'ами — все ошибки сразу на виду, писать нужно минимально.
Mixail
11.10.2017 15:36Так же можно посмотреть на redux-api-middleware
parakhod Автор
11.10.2017 17:11Их много таких готовых.
Но моя цель была не дать ссылки на какие-то существующие библиотеки, а показать, что каждый при необходимости может легко написать своё middleware конкретно для своей задачи, и что это очень просто и никакие искусственные усложнения при этом совсем не требуются.
LiguidCool
12.10.2017 15:24«И отправим Redux туда, где ему самое место — в топку! =D» ©
Ну а если серьезно… Да это модно и потому redux пихают бездумно в каждый проект, хотя на самом деле он в принципе нужен далеко не всегда и писать на том же React можно вполне без него. Более того это некислое усложнение кода зачастую и вместо его облегчения начинают наоборот лезть костыли.parakhod Автор
12.10.2017 15:29Чистейшая правда. И в 70% случаев redux можно заменить локальными стейтами или стейтом корневого компонента, раздавая его как пропсы вниз.
К сожалению, сейчас слишком многие привыкли усложнять на ровном месте.
Я обычно использую redux в крупных проектах — всегда надо смотреть на то, больше будет кодить если ты добавишь какую-то технологию, или же меньше. Если больше — нафиг такие улучшения.
vasIvas
Варварство, это когда экономят на предусмотренных логикой событиях, а в остальных местах рендерят по десять раз за frame. Если логикой определено два диспатча, значит должно быть два.