redux-mill
is the easy way to use redux by one object.
Main idea is possibility to use only one object to use actions, action creators and store.
- You hate a lot of actions constants and very long imports of these constants
- You hate a lot of equal action creators and very long imports
- You want to use reducer path once in one place
npm install --save redux-mill
It is easy to understand but there are some details.
// myReducer.js
import reduxMill from 'redux-mill';
const initialState = { title: 'Default title' };
const reducer = {
EDIT_TITLE: (state, title) => ({ ...state, title }),
// ACTION_TYPE: (state, payload, action) => ({ ...state, ...}),
SAVE: {
// it is for SAVE action
0: state => ({ state, saving: true, error: "" }),
// it is for SAVE__END action
END: (state, payload, action) => ({ ...state, saving: false }),
// it is for SAVE__FAIL action
FAIL: (state, payload, action) => ({ ...state, saving: false, error: payload.error })
}
// ...
};
export const store = reduxMill(
initialState,
reducer,
'myStoreName',
{
// debug: true,
// stateDebug: true,
// nameAsPrefix: false,
// if nameAsPrefix is true, then your action types will looks like -
// `{storeName}{divider}{actionType}` => `myStoreName__EDIT_TITLE`
// or `myStoreName__SAVE__FAIL` for children actions
divider: "__",
// it is divider between children and parent parts of key - SAVE__FAIL
mainKey: 0
// it is children key that means reaction for parent action
// LOAD: { 0: state => ({ ...state }) }
// is the same that
// LOAD: state => ({ ...state })
}
);
// selectors
export const selectTitle = store(state => state.title);
// ...
// store.js
import { createStore, applyMiddleware } from "redux";
import { store: myReducer } from './myReducer';
import { store: mySecondReducer } from './mySecondReducer';
const reducers = {
...myReducer,
// it will be 'myStoreName' key
...mySecondReducer
};
export default createStore(
combineReducers(reducers),
applyMiddleware(...)
);
import React, { PureComponent } from 'react';
import { connect } from 'react-redux';
import myReducer, { selectTitle } from './myReducer';
class MyComp extends PureComponent {
onClick = () => {
const { changeTitle } = this.props;
changeTitle('Title ' + Math.random());
}
render() {
return (
<div>
<h3>{title}</h3>
<button onClick={this.onClick}>
</button>
</div>
)
}
}
export default connect(
(state) => ({
title: selectTitle(state),
}),
{
changeTitle: myReducer.EDIT_TITLE
// myReducer.EDIT_TITLE is action creator function(payload)
}
)(MyComp);
It will be object with the same structure, but each value will be action creator function(payload).
import reduxMill from 'redux-mill';
const reducer = {
EDIT_TITLE: (state, title) => ({ ...state, title }),
SAVE: {
0: state => state,
END: state => state,
FAIL: (state, payload) => ({ ...state, ...payload })
}
};
export const store = reduxMill(
initialState,
reducer,
'myStoreName',
{ divider: "__" },
);
// what will be in `reducer` object after reduxMill calling
{
EDIT_TITLE: function(payload) {
this._ = 'EDIT_TITLE';
this.type = 'EDIT_TITLE';
this.toString = function() { return 'EDIT_TITLE' };
this.valueOf = function() { return 'EDIT_TITLE' };
return { type: 'EDIT_TITLE': payload };
},
SAVE: function(payload) {
this.END = function(payload) {
this._ = 'SAVE__END';
this.type = 'SAVE__END';
this.toString = function() { return 'SAVE__END' };
this.valueOf = function() { return 'SAVE__END' };
return { type: 'SAVE__END': payload };
},
this.FAIL = function(payload) {
this._ = 'SAVE__FAIL';
this.type = 'SAVE__FAIL';
this.toString = function() { return 'SAVE__FAIL' };
this.valueOf = function() { return 'SAVE__FAIL' };
return { type: 'SAVE__FAIL': payload };
},
this._ = 'SAVE';
this.type = 'SAVE';
this.toString = function() { return 'SAVE' };
this.valueOf = function() { return 'SAVE' };
return { type: 'SAVE': payload };
}
};
// hot it can be used
console.log(reducer.EDIT_TITLE == 'EDIT_TITLE')
// console:> true
// (!!!) but
console.log(reducer.EDIT_TITLE === 'EDIT_TITLE')
// console:> false
console.log(reducer.EDIT_TITLE._)
// console:> EDIT_TITLE
console.log(reducer.EDIT_TITLE.type)
// console:> EDIT_TITLE
console.log(String(reducer.SAVE.FAIL))
// console:> SAVE__FAIL
console.log(reducer.EDIT_TITLE({ a: 10 }))
// console:> { type: 'EDIT_TITLE', payload: { a: 10 } }
console.log(reducer.SAVE.END({ a: 10 }))
// console:> { type: 'SAVE__END', payload: { a: 10 } }
So reducer
can be used as action type for redux-saga
takeEvery(reducer.EDIT_TITLE, ...);
or as action creator
reducer.EDIT_TITLE(newTitle);
reduxMill(initialState
, reducer
, storeName
, options
): function(selector: function(store, ...args));
args | type | default | description |
---|---|---|---|
initialState |
Object | - | default state |
reducer |
Object | - | Object or instance of your class |
storeName |
String | - | it is name for redux store |
options |
Object | {...} | additional options |
args | type | default | description |
---|---|---|---|
debug |
Boolean | false | enable debug console logging |
stateDebug |
Boolean | false | enable debug state changes console logging |
divider |
String | '_' | it is divider between parent key and children sub-key |
nameAsPrefix |
Boolean | false | enable adding storeName as prefix for action types - {storeName}{divider}{actionType} |
mainKey |
String | 0 | it is key for main handler for parent action name |
reducerWrapper |
Function(initState, Function) | It is function wrapper for your reducer (e.g. immer) | |
actionWrapper |
Function(Function) | It is function wrapper for each action | |
actionCreatorWrapper |
Function(Function, actionType) | It is function wrapper for each action creator |
const store = reduxMill(initialState, reducer, 'storeName');
const selectItem = store((state, id) => state.items[id]);
// it returns function(state, id)
const selectItem2 = (state, id) => state['storeName'].items[id];
// selectItem the same that selectItem2
// state['storeName'] - is our current store
// ...
If you want to wrap your reducer, then you should use reducerWrapper
. When we make reducer function we will wrap it into reducerWrapper(initialState, reducerFunction)
.
reducerWrapper
should return Function(state, action)
.
import reduxMill from 'redux-mill';
function wrapReducer(baseState, callback) {
// callback - it is reducer function(state, action), returns state
console.log('your reducer was wrapped', baseState, callback);
return (state = baseState, action) => {
console.log('reducer called with action: ', action);
console.log('we will generate new state each time!');
// don't repeat this trick with JSON.parse(JSON.stringify(...))
// it will be applied for any result of reducer
// and for current state if this reducer doesn't contain required action
return JSON.parse(JSON.stringify(callback(state, action)));
}
}
const initialState = {
title: '',
items: [
{ note: '11111' },
]
}
const reducer = {
setTitle: (draft, payload) => { draft.title = payload }
setNote: (draft, payload) => { draft.items[0].note = payload }
}
const store = reduxMill(initialState, reducer, 'storeName', { reducerWrapper: wrapReducer });
If you want to wrap each action handler separately then you should use actionWrapper
. Each action handler will be wrapped into actionWrapper(actionHandler)
. We expect that actionHandler
is Function(state, action). actionWrapper
should return Function(state, [payload, action])
.
import reduxMill from 'redux-mill';
function wrapAction(callback) {
// callback - it is action function(state, payload, action), returns state
console.log('your action handler was wrapped', callback);
return (state, payload, action) => {
console.log('Action was called: ', action.type);
console.log('we will generate new state');
// I don't recommend to use this trick with JSON.parse(JSON.stringify(...))
// But you can, because it will be called only for actions, not for default
return JSON.parse(JSON.stringify(callback(state, payload, action)));
}
}
const initialState = {
title: '',
items: [
{ note: '11111' },
]
}
const reducer = {
setTitle: (draft, payload) => { draft.title = payload },
setNote: (draft, payload) => { draft.items[0].note = payload }
}
const store = reduxMill(initialState, reducer, 'storeName', { actionWrapper: wrapAction });
// if you wont to use actionWrapper, then you should wrap each handler by your self, it is the same
// const reducer = {
// setTitle: produceAction((draft, { payload }) => { draft.title = payload }),
// setNote: produceAction((draft, { payload }) => { draft.items[0].note = payload })
// }
If you want to wrap each action creator then you should use actionCreatorWrapper
. Each action creator will be wrapped into actionCreatorWrapper(actionCreator, actionType)
. We expect that actionCreator
is Function(payload). actionCreatorWrapper
should return Function(...anyArgs)
.
import reduxMill from 'redux-mill';
function wrapActionCreator(baseActionCreator, actionType) {
// we may return `baseActionCreator` based on any condition, e.g. `actionType` comparison
if (actionType === 'setColor') return baseActionCreator;
// baseActionCreator - it is action creator function(payload), returns { type: String, payload: Any }
console.log('your action creator was wrapped', baseActionCreator);
return (payload, addTime = false) => {
const action = baseActionCreator(payload);
if (addTime) {
action.meta = { ...action.meta, currentTime: Date.now() };
}
console.log('Action creator was wrapped for: ', action.type);
return action;
}
}
const initialState = {
title: '',
}
const reducer = {
setTitle: (state, payload, action) => ({
...state,
title: payload + ' ' + action.meta.currentTime
}),
setColor: (state, payload) => ({ ...state, color: payload })
}
const store = reduxMill(
initialState,
reducer,
'storeName',
{ actionCreatorWrapper: wrapActionCreator }
);
// In this example `reducer` will look like:
// {
// setTitle: wrapActionCreator((payload) => ({ type: 'setTitle', payload })),
// setColor: (payload) => ({ type: 'setColor', payload })
// }
Test cases for actionCreatorWrapper
import { select } from "js-split";
import mill from "./mill";
const storeName = "myItems";
const initialState = {
items: [],
title: "",
loading: false,
adding: false,
removing: false,
error: false
};
const END = "END";
const FAIL = "FAIL";
const reducer = {
EDIT_TITLE: (state, title) => ({ ...state, title }),
GET_ITEMS: {
0: state => ({ ...state, loading: true, error: "" }),
[END]: (state, payload) =>
({ ...state, loading: false, items: payload }),
[FAIL]: state =>
({
...state,
loading: false,
error: "Something is wrong with items =("
})
},
ADD_ITEM: {
0: state => ({ ...state, adding: true, error: "" }),
[END]: (state, payload) => ({ ...state, adding: false, items: payload }),
[FAIL]: state =>
({ ...state, adding: false, error: "The same item there is in list" })
},
REMOVE_ITEM: {
0: state => ({ ...state, removing: true, error: "" }),
[END]: (state, payload) =>
({ ...state, removing: false, items: payload }),
[FAIL]: state =>
({ ...state, removing: false, error: "It can't be removed" })
}
};
// Use the same handler for another action
reducer.DELETE_ITEM = reducer.REMOVE_ITEM;
export const store = mill(initialState, reducer, storeName, {
divider: "__",
mainKey: 0
});
export const selectItems = store(state => state.items);
export const selectTitle = store(state => state.title);
export const selectError = store(state => state.error);
export default reducer;
import { select } from "js-split";
import mill from "./mill";
const storeName = "myItemsM";
const initialState = {
items: [],
title: "",
loading: false,
adding: false,
removing: false,
error: false
};
const END = "END";
const FAIL = "FAIL";
const reducer = new function() {
this.EDIT_TITLE = (state, title) => ({ ...state, title });
this.GET_ITEMS = {
0: state => ({ ...state, loading: true, error: "" }),
[END]: (state, payload) =>
({ ...state, loading: false, items: payload }),
[FAIL]: state =>
({
...state,
loading: false,
error: "Something is wrong with items =("
})
};
this.ADD_ITEM_BASE = {
0: state => ({ ...state, adding: true, error: "" }),
[END]: (state, payload) => ({ ...state, adding: false, items: payload }),
[FAIL]: state =>
({ ...state, adding: false, error: "The same item there is in list" })
};
this.ADD_ITEM = {
0: state => this.ADD_ITEM_BASE[0],
[END]: (state, payload) => ({ ...state, adding: false, items: payload }),
[FAIL]: state =>
({
...state,
adding: false,
error: "The same item is in the list"
})
};
this.REMOVE_ITEM = {
0: state => ({ ...state, removing: true, error: "" }),
[END]: (state, payload) =>
({ ...state, removing: false, items: payload }),
[FAIL]: state =>
({ ...state, removing: false, error: "It can't be removed" })
};
// Use the same handler for another action
this.DELETE_ITEM = this.REMOVE_ITEM;
}();
export const store = mill(initialState, reducer, storeName, {
divider: "__",
mainKey: 0
});
export default reducer;
export const selectItems = store(state => state.items);
export const selectTitle = store(state => state.title);
export const selectError = store(state => state.error);
import { select } from "js-split";
import mill from "./mill";
const storeName = "myItemsM";
const initialState = {
items: [],
title: "",
loading: false,
adding: false,
removing: false,
error: false
};
const END = "END";
const FAIL = "FAIL";
const reducer = new class {
EDIT_TITLE = (state, title) => ({ ...state, title });
GET_ITEMS = {
0: state => ({ ...state, loading: true, error: "" }),
[END]: (state, payload) =>
({ ...state, loading: false, items: payload }),
[FAIL]: state =>
({ ...state, {
loading: false,
error: "Something is wrong with items =("
})
};
ADD_ITEM = {
0: state => ({ ...state, adding: true, error: "" }),
[END]: (state, payload) => ({ ...state, adding: false, items: payload }),
[FAIL]: state =>
({ ...state, adding: false, error: "The same item there is in list" })
};
REMOVE_ITEM = {
0: state => ({ ...state, removing: true, error: "" }),
[END]: (state, payload) =>
({ ...state, removing: false, items: payload }),
[FAIL]: state =>
({ ...state, removing: false, error: "It can't be removed" })
};
// Use the same handler for another action
DELETE_ITEM = this.REMOVE_ITEM;
}();
export const store = mill(initialState, reducer, storeName, {
divider: "__",
mainKey: 0
});
export default reducer;
export const selectItems = store(state => state.items);
export const selectTitle = store(state => state.title);
export const selectError = store(state => state.error);