Yet another way to scale reducers.
With npm:
npm i --save combine-cross-slice-reducers
Or with yarn:
yarn add -D combine-cross-slice-reducers
The usage of combineCrossSliceReducers
is the same as Redux's combineReducers's one.
A cross-slice reducer receives a third argument which is the updated global state.
The following example is based on a real-world implementation that partly inspired this module.
Let's assume we have a form reducers that are responsible for form values and enabled/visible states of form fields:
const dataUpdated = (data, user) => ({
type: 'DATA_UPDATED',
data,
user
});
const INITIAL_FORM1_STATE = {
areFieldsVisible: false,
areFieldsEnabled: false,
field1: '',
field2: true
};
const form1Reducer = (state = INITIAL_FORM1_STATE, action) => {
if (action.type === 'FORM1_DATA_UPDATED') {
const canRead = action.user.permissions.includes('READ');
return {
areFieldsVisible: canRead,
areFieldsEnabled: action.user.permissions.includes('WRITE'),
field1: action.data.key1 || '',
field2: action.data.key2 && canRead
};
}
return state;
};
const INITIAL_FORM2_STATE = {
areFieldsVisible: false,
areFieldsEnabled: false,
field1: 0,
field2: [],
field3: false
};
const form2Reducer = (state = INITIAL_FORM2_STATE, action) => {
if (action.type === 'FORM2_DATA_UPDATED') {
const canRead = action.user.permissions.includes('READ');
const canWrite = action.user.permissions.includes('WRITE');
return {
areFieldsVisible: canRead,
areFieldsEnabled: canWrite,
field1: parseInt(action.data.key1, 10),
field2: Array.isArray(action.data.key2) ? action.data.key2 : [action.data.key2],
field3: canRead && !canWrite
};
}
return state;
};
These are very basic reducers but we know that reality is more complicated than that. You usually do some kind of conversion between the data that you have received, e.g. from a backend, and what you want to display to the user. These reducers already have some duplicate logic. This can get messy very quickly.
The idea behind cross-slice reducers is to separate reducers based on their concerns. With cross-slice reducers we can write the above reducers as follows:
const INITIAL_PERMISSION_STATE = {
canRead: false,
canWrite: false
};
const permissionReducer = (state = INITIAL_STATE, action) => {
if (action.type === 'FORM1_DATA_UPDATED' || action.type === 'FORM2_DATA_UPDATED') {
return {
canRead: action.user.permissions.includes('READ'),
canWrite: action.user.permissions.includes('WRITE')
};
}
return state;
};
const INITIAL_FORM1_STATE = {
field1: '',
field2: true
};
const form1Reducer = (state = INITIAL_FORM1_STATE, action, globalState) => {
if (action.type === 'FORM1_DATA_UPDATED') {
return convertDataForForm1(action.data, globalState.permission.canRead);
}
return state;
};
const INITIAL_FORM2_STATE = {
field1: 0,
field2: [],
field3: false
};
const form2Reducer = (state = INITIAL_FORM2_STATE, action, globalState) => {
if (action.type === 'FORM2_DATA_UPDATED') {
return convertDataForForm2(action.data, globalState.permission.canRead, globalState.permission.canWrite);
}
return state;
};
function convertDataForForm1(data, canRead) {
return {
field1: action.data.key1 || '',
field2: action.data.key2 && canRead
};
}
function convertDataForForm2(data, canRead, canWrite) {
return {
field1: parseInt(action.data.key1, 10),
field2: Array.isArray(action.data.key2) ? action.data.key2 : [action.data.key2],
field3: canRead && !canWrite
};
}
While this seems to be much more boilerplate, it is still cleaner code because
- duplication of knowledge has been reduces,
- the single responsibility principle is not violated, and
- it enables easier refactoring into much smaller reducers.
To complete this example, we define our root reducer:
import combineCrossSliceReducers from 'combine-cross-slice-reducers';
const sharedReducers = {
permission: permissionReducer
};
const formReducers = {
form1: form1Reducer,
form2: form2Reducer
};
const rootReducer = combineCrossSliceReducers(sharedReducers, formReducers);
If combineReducers
does not work for you, you might want to consider other libraries as well which might fit your needs better than this library does, e.g.:
- combine-section-reducers - Very similar to this library but without stages between reducers, i.e. the global state is not modified
- reduce-reducers - Reduce multiple reducers into a single reducer from left to right
- reducer-pipe - Allows to pipe redux reducers with given state and action, passing previously returned state to next reducer, then keep last updated state
- You can find many more in Mark Erikson's curated list
MIT