Rematch and more generally Redux, encourage you to keep a flat state to solve several issues. Each model should be considered as a separate entity and should only be updated by its reducer, this is why Rematch doesn't allow nested reducers.
This being said, there is some case where nested reducers can be a good thing. In this recipe, we will see how to use reusable reducer functions.
Let's start with an example. A common pattern in Redux, when we want to store an ordered list, is "allIds, byId". Storing the entities as an array requires an additional cost of iterating over the array to find our target. On the other hand, storing the entities as an object doesn't preserve the order of items. So let's use both:
const todoList = {
state: {
byId: {
0: {
task: 'Learn Rematch',
isDone: true
},
1: {
task: 'Learn functional programming',
isDone: false
}
},
allIds: [1, 0]
}
}
byId
key stores the entity while allIds
keeps track of the order of our entities.
This creates another issue:
byId
andallIds
refers to the same entity, they definitely need to live into the same modelbyId
andallIds
don't depend on each other, they definitely need to be handled separately
Let's try to see what we get without decoupling our reducers:
const todoList = {
state: {...},
reducers: {
// update allIds and byId
remove(state, payload) {
const { idToRemove } = payload;
return {
byId: Object.entries(state.byId)
.filter(([id, todo]) => id !== idToRemove)
.reduce((acc, [id, todo]) => ({ ...acc, [id]: todo }), {}),
allIds: state.allIds.filter(id => id !== idToRemove)
};
},
// update byId only
toggle(state, payload) {
const { idToToggle } = payload;
return {
...state, // don't update allIds,
byId: {
...state.byId,
[idToToggle]: {
...state.byId[idToToggle],
isDone: !state.byId[idToToggle].isDone
}
}
};
}
}
};
We see that our reducers start to be big and pretty unreadable. Hopefully, we can separate our update functions.
We can start to isolate our pure reusable function
function filterObjectByKey(obj, f) {
return Object.entries(obj)
.filter(([key, value]) => f(key))
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {});
}
Now we can separate our reducer functions
, the functions that update a part of the state.
function removeById(state, payload) {
const { idToRemove } = payload;
return filterObjectByKey(state, id => id !== idToRemove);
}
function removeAllIds(state, payload) {
const { idToRemove } = payload;
return state.filter(id => id !== idToRemove);
}
function toggleById(state, payload) {
const { idToToggle } = payload;
return {
...state,
[idToToggle]: {
...state[idToToggle],
isDone: !state[idToToggle].isDone
}
}
}
function toggleAllIds(state, payload) {
return state;
}
And we finally mix everything all together by distibuting a part of the state to our reducers functions:
const todoList = {
state: {...},
reducers: {
remove(state, payload) {
return {
byId: removeById(state.byId, payload),
allIds: removeAllIds(state.allIds, payload)
};
},
toggle(state, payload) {
return {
byId: toggleById(state.byId, payload),
allIds: toggleAllIds(state.allIds, payload)
};
}
}
};
React example
Reducer functions make our model simpler and more readable.