1. создаем файл с константами (здесь мы сохраняем названия типов action'ов)
export const REQUEST_DATA_PENDING = "REQUEST_DATA_PENDING";
export const REQUEST_DATA_SUCCESS = "REQUEST_DATA_SUCCESS";
export const REQUEST_DATA_FAILED = "REQUEST_DATA_FAILED";
export const PROFILES_PER_PAGE = "PROFILES_PER_PAGE";
export const CURRENT_PAGE = "CURRENT_PAGE";
2. создаем файл, где описываем action'ы (здесь мы делаем запрос на получение учеток пользователей, и пагинация). Также в примере был использован redux-thunk (далее мы откажемся от подобных зависимостей):
export const requestBigDataAction = () => (dispatch) => {
fetchingData(dispatch, BIG_DATA_URL, 50);
}
export const changeCurrentPAGE = (page) => ({
type: CURRENT_PAGE,
payload: page
})
function fetchingData(dispatch, url, profilesPerPage) {
dispatch({type: REQUEST_DATA_PENDING});
fetch(url)
.then((res) => {
if(res.status !== 200) {
throw new Error (res.status);
}
else {
return res.json();
}
})
.then((data) => {dispatch({type: REQUEST_DATA_SUCCESS, payload: data})})
.then(() => dispatch({type: PROFILES_PER_PAGE, payload: profilesPerPage}))
.catch((err) => dispatch({type: REQUEST_DATA_FAILED, payload: `Произошла ошибка. ${err.message}`}));
}
3. мы пишем reducer
import { REQUEST_DATA_PENDING, REQUEST_DATA_SUCCESS, REQUEST_DATA_FAILED, PROFILES_PER_PAGE, CURRENT_PAGE } from '../constants/constants';
const initialState = {
isPending: false,
buffer: [],
data: [],
error: "",
page: 0,
profilesPerPage: 0,
detailedProfile: {}
}
export const MainReducer = (state = initialState, action = {}) => {
switch(action.type) {
case REQUEST_DATA_PENDING:
return Object.assign({}, state, {isPending: true});
case REQUEST_DATA_SUCCESS:
return Object.assign({}, state, {page : 0, isPending: false, data: action.payload, error: "", detailedProfile: {}, buffer: action.payload});
case REQUEST_DATA_FAILED:
return Object.assign({}, initialState, {error: action.payload});
case PROFILES_PER_PAGE:
return Object.assign({}, state, {profilesPerPage: action.payload});
case CURRENT_PAGE:
return Object.assign({}, state, {page: action.payload});
default:
return state;
}
}
4. настраиваем store (применяем middleware thunkMiddleware)
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import {createStore, applyMiddleware} from 'redux';
import {Provider} from 'react-redux';
import thunkMiddleware from 'redux-thunk';
import {MainReducer} from './reducers/mainReducer';
const store = createStore(MainReducer, applyMiddleware(thunkMiddleware));
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>, document.getElementById('root'));
5. подключаем компонент к redux
const mapDispatchToProps = (dispatch)=>{
return {
onRequestBigData: (event) =>{
dispatch(requestBigDataAction());
}
}
};
подключаем кнопки пагинации к redux
const mapDispatchToProps = (dispatch)=>{
return {
onChangePage: (page) =>{
dispatch(changeCurrentPAGE(page));
}
}
};
Проблема: наш редьюсер представляет собой одну большую инструкцию switch, следовательно при добавлении нового action'а, или изменения его поведения нам необходимо изменять наш редьюсер, что нарушает принципы SOlid (принцип единственной ответственности и принцип открытости/закрытости).
Решение: нам поможет полиморфизм. Добавим к каждому action'у метод execute, который будет применять обновление и возвращать обновленный state. Тогда наш reducer примет вид
export const MainReducer = (state = initialState, action) => {
if(typeof action.execute === 'function') return action.execute(state);
return state;
};
теперь при добавлении нового action'а нам не понадобиться изменять reducer, и он не превратиться в огромного монстра.
Далее откажемся от redux-thunk и перепишем action'ы
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import {createStore} from 'redux';
import {Provider} from 'react-redux';
// import thunkMiddleware from 'redux-thunk';
import {MainReducer} from './reducers/mainReducer';
const store = createStore(MainReducer);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>, document.getElementById('root'));
переходим к подключенному компоненту, action которого асинхронный (его придется совсем слегка подкорректировать)
const mapDispatchToProps = (dispatch)=>{
return {
onRequestBigData: (event) =>{
requestBigDataAction(dispatch);
},
}
};
и перейдем к самим action'ам и добавим им метод execute
const type = 'bla-bla';
const requestDataPending = {execute: state => ({...state, isPending: true}), type};
const requestDataSuccess = payload => ({
execute: function (state) {
return ({...state,
page : 0,
isPending: false,
data: payload,
error: "",
detailedProfile: {},
buffer: payload})
},
type})
const profilesPerPageAction = profilesPerPage => ({
execute: state => ({...state, profilesPerPage: profilesPerPage}),
type
});
const requestDataFailed = errMsg => state => ({...state, error: `Произошла ошибка. ${errMsg}`});
function fetchingData(dispatch, url, profilesPerPage) {
dispatch(requestDataPending);
fetch(url)
.then((res) => {
if(res.status !== 200) {
throw new Error (res.status);
}
else {
return res.json();
}
})
.then((data) => {dispatch(requestDataSuccess(data))})
.then(() => dispatch(profilesPerPageAction(profilesPerPage)))
.catch((err) => dispatch(requestDataFailed(err.message)));
}
export const requestBigDataAction = (dispatch) => {
fetchingData(dispatch, BIG_DATA_URL, 50);
}
export const changeCurrentPAGE = page => ({
type,
execute: state => ({...state, page})
})
Внимание: свойство type обязательное (если его не добавить, будет выброшено исключение). Но для нас оно не имеет вообще никакого значения. Именно поэтому у нас отпадает потребность в отдельном файле с перечислением типов action'ов.
P.S.: В данной статье мы применили принципы SRP и OCP, полиморфизм, отказались от сторонней библиотеки и сделали наш код более чистым и поддерживаемым.
Комментарии (27)
chemaxa
10.11.2019 13:46+1Если я правильно понял, то мы таким образом по пути потеряли одну из фичей редакса под названием таймтревел дебагинг?
И ещё мы теперь не знаем какие экшены и кто и когда вызвал и что они наделалиfaiwer
12.11.2019 00:27И ещё мы теперь не знаем какие экшены и кто и когда вызвал и что они наделали
справедливости ради вы и раньше не всё знали, если использовали что-нибудь вроде redux-thunk, -saga, mstp с dispatch, просто dispatch и т.д.
А TimeTravel вы не потеряете если будете отталкиваться от срезов состояний, а не от последовательностей action-ов.
kurt_live
10.11.2019 14:17+1Так, а как же разбиение модели приложения на несколько редьюсеров? В боевых приложениях обычно бывает композиция из пары десятков редьюсеров — и то, это не самые большие приложения. Тип тут нужен как индикатор будет ли изменён стейт соответствующего редьюсер. Один тип может подействовать как на 1 редьюсер, так и на 3 из 5, например. Вы убили наглядное изменение стейта, убили масштабируемость, да и солид у вас теперь не солид. Экшен знает как менять стор, знает его структуру, а редьюсер знает только execute. Сам смысл понятия редьюсер потерялся. Таким образом можно отказаться от редакса и реакт-редакса в пользу самописного варианта из одного объекта и пары функций — с учётом наличия нового контекст апи — раз плюнуть.
У вас получится свой менеджер состояний, без плюшек и девтулзов, с новой механикой.
Я работаю с редаксом 4 года и могу сказать точно:
1. Вы искалечили библиотеку
2. Вы нарушили сам принцип сингл респонсабилити
3. Ваш подход привёл к усложнению разработки. Надо бы померять ещё и производительность этого чуда.Marat1403 Автор
10.11.2019 16:14По поводу комбинации нескольких редьюсеров. В данном примере нет необходимости вообще комбинировать редьюсеры (так как причины для комбинации больше нет, редьюсер не будет разрастаться инструкциями типа switch case). В любом случае вы работатете с одним стейтом, просто при комбинации появляется поле, которое содержит подстейт (вы можете в самом экшене деструктурировать это поле и обработать нужный подстейт). Никаких редьюсеров дополнительно писать не придется.
Что касается принципа SRP, здесь я с вами не согласен. SRP гласит что должна быть только одна причина для изменения (при использовании инструкции switch количество потенциальных причин для изменения редьюсера = количеству экшенов). Для проверки выполнения этого принципа предлагаю простой прием — если возможна ситуация при которой возможны конфликты (например вы правили один экшен, другой разработчик правил другой и добавил еще один экшен) — скорее всего SRP не был соблюден.
kurt_live
10.11.2019 22:49+1Мой предыдущий ответ был съеден хабром, он был остроумен и я старался как мог. но увы, воспроизвести это великолепие я не могу.
краткие тезисы
- Редакс — это фп библиотека, переопределяя редьюсер как функцию с сайдэфектом(дергает некий колбек), вы нарушаете чистоту функции редьюсера. если раньше она отвечала за S' = f(S, A) то теперь она просто обертка вокруг вашего кода. сам редакс построен на композиции чистых функций и иммутабельности, ваш код это теряет. как теряет и масштабируемость, о чем я писал выше.
- Тип экшена болтается теперь неприкаянный, убрать вы его не можете,
мамаредакс заругает, но и применить тоже. Это уже вопит о том, что вы неправильно обращаетесь с библиотекой. если же убрать тип — не понятно, зачем экшены и диспатч в принципе. Тут стоит вставить эту надоевшую всем диаграмму реакт/редакс дата флоу, но я не буду. Без диспатча и типа экшена получается эдакое окно в Европу — ворочу, что хочу. Зачем тогда сложный редакс с его диспатчем, мидлварами и прочим — мне не ясно. Можно написать сервис в пяток строк который будет жрать функции и применять их на некий стейт. Минус еще 2 зависимости, а? - Экшен, который был просто данными(если утрировать, то это адрес шины данных и данные для этой шины), превратился в супер качка и грозит навалять очкарикам ударной левой, что его чмырили за отсутствие функционала. он уже отжал у них не только учебники, но и карманные деньги на месяц
- Вы удалили мидлвар в 13 строк и раздали ее обязанности непричастным. Думаю, redux-thunk многих смущает своим всемогуществом. Однако, дело редаксовых мидлвар — преобразовывать экшены или их предтечи, делая из сложного(например функции с сайдэффектом, как это делает thunk, или из промиса или генератора) простое — набор данных(экшенов). У вас преобразованием занялся редьюсер, а работой редьюсера занялся экшен. и тот и другой не подходят для этого. При этом, механизм мидлвар никуда не делся. То как вы трактуете солид и его применение — мягко говоря, не канон. Я привел бы такой пример: представьте маленький офис, в котором трудятся трое — офис менеджер — следит за уютом, наличием интернета, оборудования, кофе и плюшек, проект-менеджер — занимается всей проектной работой, коммуникацией по проекту, срокам, цене, требованиям и т.д. и программист — преобразует кофе, плюшки и требования в код к определенному сроку, отправляя все изменения по интернету в удаленный репозиторий. Каждый при деле, работа кипит. Приходит заказчик, который раньше присылал только требования, и увольняет программиста с целью оптимизации, менеджерам же он раздает его обязанности со словами: мне кажется программист подворовывал кофе и плюшки, я попросил его уйти по-хорошему. Зарплату вам поднять? не-не. Такая вот остросоциальная драма в коде.
- Инструменты преследуют своей формой какую-то цель, а не принцип. да, они используют принципы, но чтобы отсечь сложность и дать определенный функционал, а не пишутся, чтобы использовать некий принцип. Уверен, авторы в курсе про солид, грасп, ягни, кисс, драй и прочих зверушек и читали Мартина. Но они выбрали иные принципы, потому что они делают универсальный и надежный инструмент, который позволит делать еще более сложные и увлекательные вещи. В конце концов, на те же принципы они могут иметь свой взгляд отличный от вашего.
- Редакс и так хорош, особенно если обуздать боллерплейт. Вы можете улучшить девелопер экспириенс, уменьшив количество боллерплейта, при этом не ломая библиотеку через бедро с криком "SOLID!".
- Вы вполне можете применить солид в собственном стейт-менеджере, не мучая котиков(в данном случае редакс). Да, хайпануть на редаксе не получится, но получится хайпануть на своем коде и солиде. Больше плюсов, чем минусов.
Конечно, в съеденном хабром ответе был еще разбор на примере микросхем 16-битного ЭВМ, но такова судьба — вам он не достался.
Прошу понять, я не бультерьер, я кинолог. В смысле, я люблю ценю и уважаю хороший код, а не стараюсь абузить разрабов. Хотел бы, чтобы моя писанина вам пригодилась. И не обижайтесь на стиль и метафоры, я не умею писать сухие академические тексты, нескольких метафор и одной формулы на текст вполне хватит.
xGromMx
10.11.2019 23:53в каком месте это фп?
kurt_live
11.11.2019 08:25В том как он себя ведет и какие принципы использует — это фп. да, это не хаскель, поэтому тру фпшники могут пинать меня сколько угодно.
однако,
- все апи редакс это функции(applyMiddleware, compose, createStore, bindActionCreators, combineReducers), которые принимают другие функции или простые объекты(это допустимо в js и не делает из библиотеки не ФП).
- редакс использует чистые функции(reducers)
- редакс ожидает иммутабельного состояния
- редакс использует карирование
- редакс использует композицию функций, а не наследование.
- редакс не содержит классы в исходном коде — только плейн объекты и функции. стор это плейн объект.
- разработчики редакса сильно не рекомендуют использовать наследование, инстансы классов и прочее ооп при работе с библиотекой — сломается сериализация и таймтревел. https://redux.js.org/faq/design-decisions#why-doesnt-redux-support-using-classes-for-actions-and-reducers как пример
- this встречается часто, но на использование в коде — 3 раза, 1 раз, когда нужно вернуть ссылку на обсервабл(он внезапно тоже не инстанс какого-то класса, а просто {}) из него же https://github.com/reduxjs/redux/blob/master/src/createStore.ts#L332, что понятно — у него нет имени, и 2 раза — как имя параметра функции https://github.com/reduxjs/redux/blob/master/src/bindActionCreators.ts#L12. остальное — комментарии и строки с сообщениями об ошибках. это не противоречит фп на жс.
Если это не фп, то мне очень интересно услышать вашу версию.
VolCh
11.11.2019 09:51Ядро редакса — это чистое ФП., "грязь" начинается там, где хранится история, ну и, как я подозреваю, там где приходится обходить недостатки языка, типа неимплементированной оптимизации хвостовой рекурсия.
mayorovp
11.11.2019 14:15переопределяя редьюсер как функцию с сайдэфектом(дергает некий колбек), вы нарушаете чистоту функции редьюсера
Ну не всё так плохо: если потребовать от колбека чистоты — то вызов этого колбека также никакой чистоты не нарушит.
kurt_live
11.11.2019 14:34это все теория. на практике проверить, что колбек не вызовет сайдэффектов, нельзя. поэтому в редаксе определен слой, где эти сайдэффекты легальны — это мидлвары. в остальных местах звать некие колбеки — пусть и даже теоретически чистые — архитектурно неприемлемо. это опять к метафоре про офис и обязанности. возможно такое — вполне, правильно ли это — отнюдь
mayorovp
11.11.2019 14:42А зачем на практике проверять, что колбек не вызовет сайдэффектов? Их надо просто без сайдэффектов писать.
aleki
10.11.2019 23:35На один action могут реагировать несколько reducer’ов, в вашей же реализации это невозможно. Этого уже было достаточно, чтобы не городить этот огород.
Marat1403 Автор
11.11.2019 00:15+1В моей реализации в принципе нет нескольких редьюсеров. Каждый экшен получает весь стейт и способен обрабатывать как один так и несколько подстейтов (ведь при комбайне вы получаете объект, каждому полю которого соответствует некий подстейт). Отпадает ситуация, когда бизнес-логика одного экшена размазывается по нескольким редьюсерам.
Ogra
11.11.2019 05:33Обрабатывать несколько подстейтов в одном редьюсере — тоже серьезное нарушение single responsibility
Вы пытаетесь изолировать ответственность по экшенам, а combineReducers изолирует ответственность по подстейтам.
Из этой идеи можно запилить свою свою библиотеку, но вот с redux так обращаться не стоит.
VolCh
11.11.2019 09:46Когда слабосвязанные аспекты бизнес-логики одного экшена запихнуты в один редьюсер как раз и нарушается SRP.
faiwer
12.11.2019 00:34На один action могут реагировать несколько reducer’ов, в вашей же реализации это невозможно
Это единственное что мне понравилось в статье. Исповедую этот подход с самого начала работы с redux. На мой взгляд эта возможность сильно избыточна и как раз уводит нас от predictability. Более того она заставляет бегать по всем reducer-ам на любой чих. Статичная схема где у каждого action-type-а есть конечная "дестинация" намного проще в отладке и разработке.
bgnx
12.11.2019 13:42Я тоже когда использовал redux к этому пришел. Один экшн — обработка в одном единственном редюсере. И никаких combineReducers. Но я пошел еще дальше. Вместо неудобных switch-ей я просто решил экспортировать функции
export const createTodo = (state, action) => ...
а главный редюсер просто импортирует и вызывает
import * as actions from "./actions" export const rootReducer = (state, action) => actions[action.type](state, action)
А потом в отдельных редюсерах я решил вынести болерплейтный код в функции-хелперы — так как любую сложную логику всегда можно разбить на набор crud-операций над отдельными сущностями — то вместо того чтобы писать этот болерплейт дектруктуризации вручную я создал хелперы create(), update(), delete() которые обновляют объекты в нормализированном сторе.
Например вместо такого кода
export const createTodo = (state, action)=>{ const newTodoId = Math.random() return { ...state, users: { ...state.users, [state.currentUserId]: { ...state.users[state.currentUserId], newTodoText: "", todos: [...state.users[state.currentUserId].todos, newTodoId] } } todos: { ...state.todos, [newTodoId]: { id: newTodoId, text: state.users[state.currentUserId].newTodoText } } } }
я стал писать только саму логику а весь болерплейт связанный с деструктуризацией был вынесен в хелперы и перестал смешиваться основной с логикой
export const createTodo = (state, action)=>{ const newTodoId = Math.random(); state = create("todos", newTodoId, { id: newTodoId, text: state.users[state.currentUserId].newTodoText }); state = update("users", state.currentUserId, { newTodoText: "", todos: [...state.users[state.currentUserId].todos, newTodoId] }); return state; }
А потом я подумал — если каждому экшену соотвествует только один редюсер то зачем логику держать в редюсере — я ведь могу создать 3 редюсера create(), update(), delete() а саму логику держать там где она требуется — например рядом с кнопкой. И это удобно — так как а) получаем меньше действий навигации по исходникам чтобы понять что делает конкретная кнопка б) когда удаляем кнопку не нужно помнить что где-то в другом файле нужно не забыть почистить и удалить лишний редюсер
<button onClick={()=> const newTodoId = Math.random(); create("todos", newTodoId, { id: newTodoId, text: getState().users[getState().currentUserId].newTodoText }); update("users", getState().currentUserId, { newTodoText: "", todos: [...getState().users[getState().currentUserId].todos, newTodoId] }) }}> add todo </button>
Ну а дальше я стал еще больше убирать болерплейт и добавлять удобства — вместо того чтобы передавать хелперам имя таблицы в нормализирвоанном сторе я создал фабрики для таблиц — и вместо
create("todos", newTodoId, {..})
получилосьtodos.create(newTodoId, {...})
— теперь опечататься уже не получится, плюс имеем автокомплит
А дальше вместо того чтобы возиться с айдишниками я сделал врапперы над объектами которые скрывают болепрейт постоянного вытаскивания объекта по айдишнику — то есть вместо того чтобы получать временный текст тодошки у текущего юзера таким образом
getState().users[getState().currentUserId].newTodoText
я просто обращаюсь к геттеру у объекта текущего юзера
currentUser.newTodoText()
а внутри геттера newTodoText() будет происходить чтение по айдишнику своего объекта нужного свойства в нормализирвоанном сторе. Ну и добавив к объекту врапперу метод update(), получим удобный способ обновлять поля объектов. В итоге пример с добавлением тодошки стал записываться примерно так
<button onClick={()=> const newTodo = state.todos.create({text: currentUser.newTodoText()}); currentUser.update({newTodoText: "", todos: [...currentUser.todos(), newTodo]}) }}> add todo </button>
И если на примере тодошки болерплейт связанный с постоянным вытаскиванием по айдишникам не сильно заметен то когда количество сущностей со связями one-to-many становится больше — например юзер может создать папки в папках борды в бордах проекты а в проектах тодошки то чтобы прочитать свойство папки имея объект тодошки нам нужно несколько раз вытаскивать по айдишнику родительские сущности и такой код становится просто нечитаем
state[state[state[todo.projectId].boardId].folderId].someField
а если вынести этот болерплейт в объекты-врапперы то получаем вполне себе читабельный код
todo.project().board().folder().someField
А дальше я увидел mobx где получаем все то же самое к чему я пришел но проще и эффективней без ненужной иммутабельности которая на тот момент стала лишь технической деталью потому что внешний интерфейс стал напоминать работу с графом объектов связанных между собой ссылками — можно пройтись по ссылкам из одного объекта к другому как например из глубокой сущности к родительской
task.project().folder().user().someField
, так и обновить нужное свойство на любой глубине —task.update({text: newText})
faiwer
12.11.2019 14:36Собственно да, вы шаг за шагом ушли от redux way в сторону чего-то вроде MobX. Думаю вы бы сэкономили себе много времени если бы использовали его с самого начала. Я тоже до неузнаваемости искажаю работу с redux, избавляясь от того, что я считаю лишним, и добавляя то, что считаю необходимым. Но суть — one way data flow + signals — не изменяю, остаюсь при ней.
У меня:
- нет action type констант, т.к. они создают больше проблем чем решают (в приложениях любого масштаба).
- нет combine reducer-ов, вместо этого — просто древо reducer-ов где каждому action-у задан конкретный reducer-handler, путь к которому определяется по action type.
- в reducer-ах всегда доступен 3-им аргументом rootStore для чтения
- immer разумеется, куда без него в 2019, не писать же………… везде
- actionCreator-ы и их reducer лежат в одних и тех же файлов (никаких import hell), чаще всего селекторы лежат там же
- для переносимого кода всё это оборачивается в фабрики
- отдельный велосипед позволяющий избежать привязки к конкретному месту в state tree абстрагируя это на уровне выше. по сути redux код модуля ничего не знает о том, где лежит его стейт, но при этом все его selector-ы работают исправно
Ну и многое другое. С бухты барахты надо ещё догадаться, что это всё ещё redux. Ведь почти нет boilerplate кода, нет тонны однообразной копипасты в actionCreator-ах (использую фабики POJO actionCreator-ов), нет этой switch-case лапши в reducer-ах, нет десятков путанных файлов, когда 1 функциональность рассеяна по кодовой базе. Много чего удаётся избежать.
При этом time machine и redux dev tools — работают, immutability — на месте, predictability — даже лучше, one way data flow — на месте. Но несколько больше специально введённых ограничений. В итоге система в большей степени устойчива в хаосу.
Marat1403 Автор
Нам вообще не нужен тип экшена, каждый экшен имеет свой метод execute, и вместо того чтобы проверять его тип и в зависимости от типа выполнять преобразование, мы просто вызываем метод execute, а экшен сам знает что делать (то есть мы отходим от процедурного программирования к объектно-ориентированному
funca
Экшен должен быть простой структурой данных, которую можно сериализовывать. Собственно из-за этого в редаксе столько церемоний. Объект с методами таким свойством не обладает.
Для композиции редьюсеров "как в солид" можно пользоваться трансдьюсерами, например подключив recompose. Главное понимать, зачем это вам нужно.
JustDont
Скажем так, организовать трансляцию чего-то сериализующегося в объекты и обратно — это не rocket science. Всю дорогу так делали, начиная с древнейших времен.
Тут скорее вопрос в том, что нам важнее — чистая сериализация и постоянные ужимки в архитектуре кода (потому что мы теперь пляшем вокруг потребностей сериализации даже в тех областях, которые сами к сериализации никаким боком), или же сериализация у нас отдельным слоем логики со своей собственной магией, но зато остальной код совсем не парится этими проблемами.
Ну а статья — она вообще, на мой взгляд, очень такая себе. Редакс это opinionated библиотека, что означает, что в принципе оно всё работает, если делать строго так, как задумано авторами. Делать по-другому? А зачем, когда можно просто не пользоваться редаксом? Автор выкинул редьюсеры (логичный шаг с т.з. упрощения архитектуры), и с этим потерял все фичи редакса, которые на этом основаны — ну и зачем тогда редакс вообще?
faiwer
Не должен. Здесь нет догм. POJO это типовое решение. Но не единственное. Но оно, к примеру, позволяет обмениваться action-ми между клиентом и сервером.
В redux вообще мало этих самых "должен". Там просто есть набор рекомендаций, идеология, и типовые примеры. Вы не обязаны использовать switch-case, вы не обязаны использовать action-constant-type-ы, вы не обязаны использовать combine-reducers (по сути вы можете слепить какой-угодно редьюсер, с какой угодно схемой). Вы даже не обязаны использовать connect. Вам просто дана шина данных и набор советов как ею можно воспользоваться. Не более.