Комментарии 66
import { createAction } from 'redbox'
export const initialState = {
TODO: []
}
export const addTODO = createAction((state, payload) => {
return {
...state,
TODO: [
...state.TODO,
payload
]
}
})
Это и есть экшн + редюсер. При вызове addTODO('do some stuff') в state добавится этот элемент.
Я делал либу в первую очередь для себя и для использования в реальном проекте и это помогло сократить код в разы
Это отлично, но, по-сути, вы сделали
reflux
поверх redux
, хотя в последнем намеренно избегается такой подход.Неявная тут реакция стейта на экшен, т.к. стейт у вас теперь «как бы упакован» в функцию addTodo, когда по философии redux необходимо явно описать реакцию в редьюсере.
Как вот быть, если нужно в нескольких местах в общем стейте отреагировать на 1 экшен addTodo без изменения непосредственно addTodo?
Если же я правильно понял и вы хотите просто изменить основное дерево state в нескольких местах при вызове одного экшна что мешает вам вызвать несколько экшн-редьюсеров? Пример в этом комментарии
Это совсем другое, чем что? Чем RedBox, о котором пишет автор этой статьи на Хабре? Тогда да, конечно. Чем подход, о котором пишет человек на Медиуме по ссылке. Тогда нет, не согласен. Довольно близкие идеи и реализация.
Хабр как всегда… лишь бы обосрать, а не написать по делу. Можно придраться еще к тому что я использовал 'red' в начале названия… идиотизм
Мне кажется у всех могло бы возникнуть меньше вопросов или более конкретизированные.
Плюс многие из нас код читают лучше чем статьи :)
Исходники тут: https://github.com/pavelivanov/redaction/tree/master/example
Позже добавлю тесты
обвинение в краже здесь некорректно.
Но все же получается конфуз, когда есть две библиотеки с одинаковым названием, но разным назначением.
В проекте и использованием обоих будет сложновато объяснять какой именно редбокс имелся в виду.
Ну и, справедливости ради, библиотека, показывающая красный квадратик более соотвествует названию, чем утилита для создания экшенов.
А что касается красного квадратика… квадратик это Square на английском, если уж на то пошло… А Box — коробка.
Для справедливости — она и чаще прямоугольничек показывает, чем квадратик, потому пусть называется RedRectangle. Хотя еще более семантически подходит RedBackground. Не парьтесь, все нормально, это что-то ребятам делается сегодня, что они так в это название вцепились.
Спасибо за объяснение этимологии вашего названия, ваша идея стала понятнее.
так удобно же.
Поправил скрипт, сохранил, увидел. Когда экран маленький и нет места для консоли — самое то.
Разумеется, для продакшена эту фичу нужно отключать.
не понимаю вопрос.
А как надо?
У меня в консоли ничего не кликается (пользуюсь стандартым терминалом MacOs).
Более того, оказывается в rebox можно сделать ссылки кликабельными, но я этим никогда не пользовался
А к вопросу стрёмных текстовых констант вот — https://github.com/pauldijou/redux-act
В целом я не ушел далеко от стандартов, просто упаковал их в сахар.
import actions from 'core/actions'
export const doMultipleActions = createAction((state, { foo, bar }) => {
actions.reducersFolderName.setFoo(foo)
actions.reducersFolderName.setBar(bar)
})
export const initialState = {
foo: null,
bar: null
}
export const setFoo = createAction((state, payload) => ({ ...state, foo: payload }))
export const setBar = createAction((state, payload) => ({ ...state, bar: payload }))
doMultipleActions({
foo: 1,
bar: 2
})
Это, конечно, выглядит не совсем корректно с точки зрения использования редьюсера, но работать будет как часы.
Давайте представим, что у нас есть несколько компонент: один предоставляет юзеру возможность логиниться, второй показывает прогрессбар, третий отображает ошибки. И чтобы они никак не были связаны между собой — каждый следит только за своим кусочком стейта. То есть предполагается, что ошибки и процессы могут быть из кучи других мест.
И вот юзер вводит логин-пароль, вызываем экшен API_REQUEST, который шлет запрос через какую-нибудь миддлварь —> компонент процесса показывает прогрессбар —> приходит ответ сервера с ошибкой авторизации —> Прогрессбар пропадает —> ошибка висит в углу пока ее не скроют или она не перестанет быть актуальной —> у формочки логина тоже отображается ошибка.
Если у нас отделены редюсеры от экшенов, то это легко реализовать, подписав каждый редюсер на нужные нам экшены: редюсер юзера (из стейта которого берет данные об ошибках формочка) ждет юзердату / ошибку в API_RESPONSE, прогрессбар положил в стейт данные на API_REQUEST и убрал их на API_RESPONSE, а редюсер ошибок положил ошибку, получив ее в API_RESPONSE.
Итого у нас три несвязанных компонента со своими стейтами и своей логикой, которые фактически слушают одни и те же события: два экшена.
В вашем же случае, как я понимаю, придется создать для каждого компонента отдельные экшено-редюсеры и явно комбинировать их в «мега экшене» (типа doMultipleActions). И мне почему-то кажется что в редкс-девтулз мы увидим, как задиспатчился этот самый «мега экшен», а тот в свою очередь задиспатчил еще несколько. Т.е. мы получаем более сложную хистори, которую в случае чего будет сложнее откатывать (если речь пойдет не про логин, а скажем про редактирование чего-нибудь с возможностью у юзера сделать анду).
Надеюсь я понятно изложил пример. Если я не прав, то поправьте меня. Спасибо.
// actions
export const login = (username, password) => (dispatch) => {
dispatch({
type: 'API_REQUEST',
payload: {
uri: '/login',
body: { username, password },
},
});
setTimeout(
() => dispatch({
type: 'API_RESPONSE',
payload: {
uri: '/login',
response: 401,
},
error: true,
}
), 5000);
};
// users/reducer
export default (userState = {}, action) => {
if (
action.payload.uri === '/login' &&
action.type === 'API_RESPONSE'
) {
if (action.error) {
return {
error: 401,
};
}
userState = action.payload.response;
}
return userState;
};
// progress/reducer
export default (progressState = [], action) => {
if (action.type === 'API_REQUEST') {
progressState.push(action.payload.uri);
}
if (action.type === 'API_RESPONSE') {
const i = progressState.indexOf(action.payload.uri);
progressState.splice(i, 1);
}
return progressState;
};
// errors/reducer
import { ERROR_MESSAGES } from './messages';
export default (errorsState = [], action) => {
if (action.type === 'API_RESPONSE' && action.error) {
errorsState.push({
requestUri: action.payload.uri,
message: ERROR_MESSAGES[action.payload.response],
});
}
return errorsState;
};
// rootReducer
import users from '/users/reducer';
import progress from '/progress/reducer';
import errors from '/errors/reducer';
export default combineReducers({
users,
progress,
errors,
});
А компоненты, соответственно получают эти кусочки стейтов (с помощью селекторов / курсоров выдергивают только свои) и:
- формочка показывает ошибки, если они есть или закрывается если есть юзердата
- прогрессбар в данном случае просто крутилка, которая отображается пока длина массива больше 0
- ошибки показываются, если есть что показать (в данном примере без удаления и проверок на актуальность)
// actions/user.js
export const login = createAction({
endpoint: '/login',
method: 'POST'
})
// containers/Login/index.js
@connect((state) => ({
authentication: state.user.authentication
}))
export default class Login extends React.Component {
auth = () => {
actions.auth.login({
subset: 'authentication',
body: {
username,
password
},
onResponse: ({ body }) => {
// запускаем любой другой асинхронный вызов, если нужно
}
})
}
render() {
const { authentication } = this.props
if (authentication.pending) {
return (
<Loader />
)
}
if (authentication.error) {
return (
<div>Authentication ERROR</div>
)
}
return (
<div>
<AuthForm onSubmit={this.auth} />
</div>
)
}
}
Если вы имеете ввиду что в моем случае нельзя при создании экшна построить сразу цепочку вызовов, то это относится к архитектуре и, имхо, я буду больше рад такому варианту с цепочкой асинхронных запросов нежели буду довольствоваться лапше из кода =/ К сожалению, практика показала что такой вариант в разы лучше. Есть огромный проект, в котором используется чистый Redux (примерно 400 апи запросов) и есть еще больше проект где используется подход Redbox и кода в разы меньше / чище и поддерживать в разы легче.
Но в вашем примере получается, что все эти три компонента — прогрессбар, ошибка и данные юзера связаны между собой (смотрят в один кусок стейта). То есть их независимость, о которой я говорил, теряется.
В моем же примере ошибки могут приходить из любых других асинхронных запросов или даже вполне себе синхронных, например, форма не прошла валидацию, аналогично и с прогрессбаром.
п.с.
Минусы не мои :)
п.с.
К минусам необоснованным на хабре я привык… пофигу)
Что мешает тогда сделать так:
// actions/index.js
import actions from './redbox-actions'
export const someAction = () => {
actions.user.authStart()
request()
.then((result) => {
actions.user.authSuccess(result)
actions.user.authComplete()
})
.catch((err) => {
actions.user.authError(err)
actions.user.authComplete()
})
//...
}
export default actions
Код ведь чище и его меньше, читать удобнее и поддерживать. Все понятно, откуда что берется.
Я к тому что суть не меняется… это все теже редьюсеры… Просто теперь не нужны типы и диспатч метод. Внутри redbox, если вы посмотрите, создаются все теже комбинированные редьюсеры, просто типы для них создаются уникальные от имени файла, в котором были созданы и имени метода, которым вы его обозвали. А диспатч передается один раз в процессе инициализации и делается замыкание
… и передаете тип вызываемого редьюсера. Чтобы вызвать 3 редьюсера придется сделать 3 диспатча.
Это утверждение не верно. В Redux есть экшены, которые изначально являются простыми объектами с определенной структурой (Flux Standart Actions), который содержит в том числе тип экшена. Тип же это любая строка, циферка или что угодно, с чем мы сможем потом сравнить (а чтобы избежать ошибок — их выносят в отдельный файлик констант). И есть редюсеры, которые комбинируются с помощью combineReducers. То есть фактически редюсер всегд один, но может состоять из подмножества небольших редюсеров.
Что происходит во время диспатча — экшен прокидывается в этот самый единственный редюсер, а тот в свою очередь прокидывает экшен внутрь всех дочерних, каждый вовзвращает свой кусочек стейта и в итоге мы получаем новый стейт.
То есть, если не связывать тайпы с редюсерами, а чтобы тайп был фактом действия (как это рекомендует сам Дэн Абрамов), то получится что экшентайпы будут выглядеть, например, так:
COUNTER_TICK
API_REQUEST
MESSAGE_SENDED
MESSAGE_READED
и т.д.
При этом любой редюсер может реагировать на любой из экшенов. В примерах Redux-а это делается с помощью switch(action.type) {… } внутри любого из них.
То есть, если не связывать тайпы с редюсерами, а чтобы тайп был фактом действия (как это рекомендует сам Дэн Абрамов), то получится что экшентайпы будут выглядеть, например, так:
COUNTER_TICK
API_REQUEST
MESSAGE_SENDED
MESSAGE_READED
и т.д.
Я вот не понимаю, зачем придумывать новые страшные названия для самых обычных «событий»? Ну кроме желания показать, что ты модный и молодежный? Может если бы не придумывали свои названия для старых вещей, то люди легче понимали б вас. Вот придумали "Presenter-Component" как синоним связке «View-Controller». Зачем? При этом все, что написано на редакс — тонкая Model. Но нет. «Мы не используем грязный MVC, как эти задроты».
Например:
Действия (аналог событий) — главное отличие от привычных нам событий, что на них нельзя подписываться для изменений данных в стейте (модели), их можно только вызывать. Но есть всякие штуки типа redux-saga, которые позволяют создавать подписки для получения неких сайд-эффектов, но это другая песня и делать это нужно осторожно.
Редюсеры (аналог моделей) — все модельки упаковываются в один стор. Главная особенность, что никто напрямую не дергает модельку, а каждая моделька сама применяет к себе действия (события) и, возможно, в следствии меняется. И стор всегда возвращает консистетное состояние всей системы (всех моделей).
Контейнеры (statefull components) (аналог контроллеров) не должен выполнять какой-то логики обработки данных, он должен ее лишь получать и назначать вьюхе (все как в правильном активном MVC). И привязывать вызов экшенов (! но не решать какую модельку когда потрогать и как). И каждый такой контейнер может работать сразу с множеством «моделей» (получать любые данные из стора), хотя привычные всем контроллеры чаще всего работают с одной моделькой. А в связке с редуксом и вовсе многие рекомендуют делать их тоже стейтлесс и все стейты хранить в общем стейте — это снимает множество боли.
Компоненты — да, привычные всем тупые вьюхи.
Отсюда следует вывод, что называть это MVC не верно, т.к. оно имеет ряд концептуальных отличий и одно из самых жирных — отсутствие pub/sub. (с тем же успехом можно MVP и MVVM называть MVC)
Не подумайте что я ярый фанат реакта, но из всех ангуляров и бекбонов он мне доставляет меньше всего боли при разработке сложных приложений (вспоминается цитата из выступления Александра Соловьева про FRP и кложуру «У меня уже все болит от этих чик-чик»). Но я все чаще смотрю в сторону FRP библиотек и языков.
export const testAction = () => ({
type: TEST_ACTION
});
А где тесты?
Redux Action Creators. Без констант и головной боли