Redux Cube is an app state manager. It is part of my effort to simplify the usage of all 'mainstream' tools and best practices mentioned in the Spellbook of Modern Web Dev. It can reduce boilerplate and support many patterns (like Sub App, Reducer Bundle, ...)
Slides: Introduction to Redux Cube
npm install --save redux-cube
Coming soon!
// sampleApp/hub.js
import { createHub } from 'redux-cube';
export default createHub();
createHub
returns a Hub
instance which is an namespace manager of action types and a set of helper functions used to generate standard action object (follow Flux Standard Action and other best practices) and action creators.
// sampleApp/actions/sample.js
import hub from '../hub';
export const { actions, types, typeDict } = hub.add('NAMESPACE/MORE_NAMESPACE/MY_TYPE');
export const { actions, types, typeDict } = hub.add('namespace.moreNamespace.myType');
export const { actions, types, typeDict } = hub.add({
NAMESPACE: {
MORE_NAMESPACE: {
MY_TYPE: true
},
},
});
export const { actions, types, typeDict } = hub.add({
namespace: {
moreNamespace: {
myType: true
},
},
});
The above codes are equivalent.
console.log(typeDict)
// {
// 'NAMESPACE/MORE_NAMESPACE/MY_TYPE': defaultActionCreator,
// }
console.log(types)
// {
// namespace: {
// moreNamespace: {
// myType: 'NAMESPACE/MORE_NAMESPACE/MY_TYPE',
// },
// },
// }
console.log(actions)
// {
// namespace: {
// moreNamespace: {
// myType: defaultActionCreator,
// },
// },
// }
The defaultActionCreator
is equivalent to:
() => ({
type: 'NAMESPACE/MORE_NAMESPACE/MY_TYPE',
payload: defaultPayloadCreator,
meta: undefined,
})
The defaultPayloadCreator
is equivalent to:
a => a
export const { actions, types, typeDict } = hub.add('NAMESPACE/MORE_NAMESPACE/MY_TYPE', payloadCreator, metaCreator);
export const { actions, types, typeDict } = hub.add({
namespace: {
moreNamespace: {
myType: true,
myType2: payloadCreator,
myType3: [payloadCreator, metaCreator],
myType4: [
(a, b) => ({ data: a + b }),
(a, b) => ({ a, b }),
],
myType5: {
[hub.ACTION_CREATOR]: actionCreator,
},
},
},
});
actions.namespace.moreNamespace.myType(10);
// or
typeDict['NAMESPACE/MORE_NAMESPACE/MY_TYPE'](10);
// results:
// {
// "type": "NAMESPACE/MORE_NAMESPACE/MY_TYPE",
// "payload": 10
// }
actions.namespace.moreNamespace.myType4(1, 10);
// or
typeDict['NAMESPACE/MORE_NAMESPACE/MY_TYPE_4'](1, 10);
// result:
// {
// "type": "NAMESPACE/MORE_NAMESPACE/MY_TYPE_4",
// "payload": { data: 11 },
// "meta": { "a": 1, "b": 10 }
// }
// sampleApp/reducers/sample.js
import hub from '../hub';
export const { reducer } = hub.handle({
namespace: {
moreNamespace: {
myType: (state, { payload, meta }) => newState,
},
},
anotherType: (state, { payload, meta }) => newState,
}, initialStateForASliceOfStore);
For common needs:
For API-related needs:
For complex needs:
// sampleApp/actions/users.js
import hub from '../hub';
// https://www.npmjs.com/package/hifetch
import hifetch from 'hifetch';
import { reset } from 'redux-form';
export const { actions, types, typeDict } = hub.add({
users: {
fetchAll: () =>
// handle by redux-promise-middleware
hifetch({
url: '/v1/users/',
}).send(),
add: [
(userId, userData, opt) =>
// handled by Thunk Payload Middleware
dispatch =>
// handle by redux-promise-middleware
hifetch({
url: `/v1/users/${userId}`,
method: 'put',
data: userData,
...opt,
}).send().then(response => {
dispatch(reset('userInfos'));
return response;
}),
userId => ({
userId,
}),
],
delete: [
(userId, userData) =>
// handle by redux-promise-middleware
hifetch({
url: `/v1/users/${userId}`,
method: 'delete',
}).send(),
userId => ({
userId,
}),
],
},
});
// handle by redux-observable
export const epics = [
action$ =>
action$.pipe(
ofType('USERS/DELETE_FULFILLED'),
map(action => ({
type: 'NOTIFY',
payload: { text: 'DELETED!' },
}))
),
];
// sampleApp/reducers/users.js
import hub from '../hub';
import Immutable from 'immutable';
export const { reducer, actions, types, typeDict } = hub.handle(
{
users: {
fetchAllPending: state => state.set('isLoading', true),
fetchAllFulfilled: (state, { payload }) =>
state.mergeDeep({
users: Immutable.fromJS(payload.data),
isLoading: false,
}),
fetchAllRejected: state => state.set('isLoading', false),
addPending: state => state.set('isLoading', true),
// ...
deleteFulfilled: (state, { payload }) =>
state.set(
'users',
state.get('users').filter(user => user.get('id') !== payload.userId),
),
},
},
Immutable.fromJS({
users: [],
isLoading: false,
}),
);
How to use redux-cube with redux-source:
See webcube-examples
Original Ducks Modular:
// widgets.js
// Action Types
// Action Creators
// Side Effects
// e.g. thunks, epics, etc
// Reducer
For reference:
Redux Cube's Reducer Bundle:
// sampleApp/ducks/actions/sample.js
import hub from '../../hub';
export const { actions, types, typeDict } = hub.add({
myType1: asyncPayloadCreator1,
myType2: asyncPayloadCreator2,
});
// sampleApp/ducks/sample.js
import hub from '../hub';
import { typeDict as existTypeDict } from './actions/sample';
export const { reducer, actions, types, typeDict } = hub.handle(
// declared action type
myType1: (state, { payload, meta }) => newState,
// undeclared action type
myType3: (state, { payload, meta }) => newState,
// undeclared action type
myType4: (state, { payload, meta }) => newState,
}, initialStateForASliceOfStore).mergeActions(existTypeDict);
export const epics = [
action$ =>
action$.pipe(/* ... */)
];
import { actions, types, typeDict } from '../reducers/sample';
console.log(actions);
// {
// myType1: asyncActionCreator1,
// myType2: asyncActionCreator2,
// myType3: defaultActionCreator,
// myType4: defaultActionCreator,
// }
console.log(typeDict);
// {
// MY_TYPE_1: asyncActionCreator1,
// MY_TYPE_2: asyncActionCreator2,
// MY_TYPE_3: defaultActionCreator,
// MY_TYPE_4: defaultActionCreator,
// }
It is highly recommended to use "duck" files as the only authoritative sources of action types and action creators.
Action files should be only used by "duck" files. They should be totally transparent to all other code.
Because hub.handle
can automatically add actions for undeclared action types, you only need to manually call hub.add
(and maybe write them in a separate action file) when these actions have side effects
// sampleApp/containers/Sample.jsx
import { connect } from 'redux-cube';
import { Bind } from 'lodash-decorators';
import { actions as todoActions } from '../ducks/todo';
@connect({
selectors: [
state => state.todo.input,
state => state.todo.items,
],
transform: (input, items) => ({
input,
items,
count: items.filter(item => !item.isCompleted).length,
}),
actions: todoActions,
})
export default class Main extends PureComponent {
@Bind
handleInputChange(content) {
this.props.actions.todo.changeInput(content);
}
render() {
const { input, items, count } = this.props;
Te above code is equal to
// ...
import { createSelector } from 'reselect';
@connect({
mapStateToProps: createSelector(
[
state => state.todo.input,
state => state.todo.items,
],
transform: (input, items) => ({
input,
items,
count: items.filter(item => !item.isCompleted).length,
}),
),
mapDispatchToProps: dispatch => ({
actions: {
todo: {
changeInput: (...args) => dispatch(
todoActions.todo.changeInput(...args)
),
},
},
}),
})
export default class Main extends PureComponent {
mapDispatchToProps
option can be used together with actions
option.
mapStateToProps
option can be used together with selectors
option.
// multipleTodoApp/todo/index.jsx
import React, { Component } from 'react';
import withPersist from 'redux-cube-with-persist';
import localforage from 'localforage';
import { createApp } from 'redux-cube';
import { reducer as sampleReducer, epics } from './ducks/sample';
import { reducer as sample2Reducer, epics } from './ducks/sample2';
import Sample from './containers/Sample';
@createApp(withPersist({
reducers: {
items: sampleReducer,
sample2: {
data: sample2Reducer,
},
},
epics,
preloadedState: typeof window !== 'undefined' && window._preloadTodoData,
devToolsOptions: { name: 'TodoApp' },
persistStorage: localforage,
persistKey: 'todoRoot',
}))
class TodoApp extends Component {
render() {
return <Sample />;
}
}
export const App = TodoApp;
// multipleTodoApp/index.jsx
import React, { Component } from 'react';
import { Route, Redirect, Switch } from 'react-router-dom';
import { createApp } from 'redux-cube';
import withRouter from 'redux-cube-with-router';
import { App as TodoApp } from './todo';
const JediTodoApp = () => (
<TodoApp
title="Jedi Todo"
routePath="/jedi-todo"
appConfig={{
persistKey: 'jediTodoRoot',
devToolsOptions: { name: 'JediTodoApp' },
preloadedState:
typeof window !== 'undefined' && window._preloadJediTodoData,
}}
/>
);
const SithTodoApp = () => (
<TodoApp
title="Sith Todo"
routePath="/sith-todo"
appConfig={{
persistKey: 'sithTodoRoot',
devToolsOptions: { name: 'SithTodoApp' },
preloadedState:
typeof window !== 'undefined' && window._preloadSithTodoData,
}}
/>
);
@createApp(withRouter({
supportHtml5History: isDynamicUrl(),
devToolsOptions: { name: 'EntryApp' },
}))
class EntryApp extends Component {
render() {
const TodoApps = () => (
<div>
<JediTodoApp />
<SithTodoApp />
</div>
);
const JumpToDefault = () => <Redirect to="jedi-todo/" />;
return (
<Switch>
<Route path="/" exact={true} render={JumpToDefault} />
<Route path="/" render={TodoApps} />
</Switch>
);
}
}
export const App = EntryApp;
Frozen plain object + immutability-helper / icepick / seamless-immutable / dot-prop-immutable / object-path-immutable / timm / updeep
@createApp(withPersist(withRouter({
reducers,
disableFreezeState: false, // default
// ...
})))
import update from 'immutability-helper';
import hub from '../hub';
export const { reducer, actions, types, typeDict } = hub.handle({
changeInput: (state, { payload: content }) =>
update(state, {
input: { $set: content },
}),
todo: {
clearCompleted: state =>
update(state, {
items: {
$apply: items =>
items.map(item =>
update(item, {
isCompleted: { $set: false },
}),
),
},
}),
},
}, {
items: [],
input: '',
});
ImmutableJS object + redux-immutable
@createApp(withImmutable(withRouter({
reducers, //
// ...
})))
import { createApp, createHub, connect } from 'redux-cube'
It's mainly a wrapper of redux API and some must-have action middlewares, store enhancers, high-order reducers and high-order components.
It provides the support for Sub-App pattern (React component with its own isolated Redux store)
Options
reducers
reducer
epics
disableDevTools
devToolsOptions
disableFreezeState
loggerConfig
- promiseMiddlewareConfig`
preloadedState
middlewares
priorMiddlewares
enhancers
priorEnhancers
storeListeners
An superset and enhanced implement (almost a rewrite) of redux-actions.
It provides the support for namespace management and Reducer-Bundle-or-Ducks-Modular-like pattern
Options:
delimiter
It's mainly a wrapper of react-redux and reselect
Options:
selectors
transform
mapStateToProps
actions
actionsProp
mapDispatchToProps