redux-mill
v1.3.0
Published
It is the easy way to use redux by one object
Downloads
18
Maintainers
Readme
redux-mill
What is it?
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 should try it if
- 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
Installation
npm install --save redux-mill
Using
It is easy to understand but there are some details.
Create your reducer
// 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);
// ...
Connecting to your store
// 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(...)
);
Using action creator in component
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);
(!) reducer
object will be changed after call of reduxMill
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);
Description
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 |
Options includes next props
| 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 |
It returns function that is factory of selectors for REDUX[storeName]
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
// ...
Use with reducerWrapper
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 });
Use with actionWrapper
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 })
// }
Use with actionCreatorWrapper
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
reducer
is simple object
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;
reducer
is instance of function
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);
reducer
is instance of class
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);