Managing the state of an application with libraries like redux
is awesome. It provides a really easy way to write simple and testable code for state transitions. You only need to decide on a structure of the desired state and write a corresponding pure function.
There is no doubt that a reducer could look super simple in the cases like the following one:
// the reducer itself
// the state is:
//
// [number]
const counter = (state = 1, action) => {
switch (action.type) {
case 'INCREMENT':
return state + 1;
default:
return state;
}
};
// get elements
const $counter = document.getElementById('counter');
const $state = document.getElementById('state');
const update = (state) => {
$counter.innerHTML = state;
$state.innerHTML = state.toString();
};
// get initial state
let state = counter(undefined, { type: 'INIT' });
// update dom
update(state);
$counter.onclick = function (e) {
const action = { type: 'INCREMENT' };
// update state with reducer
state = counter(state, action);
// update dom
update(state);
};
Our main task as developers is to keep all reducers as simple as possible. Simple reducer is easily understandable and it’s not a big deal to write a couple of helpful tests for it. But the state grows bigger and becomes more and more complex as we add more cool features to our apps. We need a technique to hide this complexity somewhere.
Every reducer is just a common pure function. No magic at all. Therefore it’s possible to write such a reducer function which will take a set of other reducers on its input to produce their results in a combination of some sort. Let’s look at a few examples:
Combine a set of reducers into a map of properties
Let’s assume that our app needs two different counters at the same time. It would be great to code something like that (with magic combineReducers
function):
// the reducer itself
// the state is:
//
// [number]
const counter = (state = 1, action) => {
switch (action.type) {
case 'INCREMENT':
return state + 1;
default:
return state;
}
};
const counters = Redux.combineReducers({
counter1: counter,
counter2: counter,
});
// get elements
const $counter1 = document.getElementById('counter1');
const $counter2 = document.getElementById('counter2');
const $state = document.getElementById('state');
const update = (state) => {
$counter1.innerHTML = state.counter1;
$counter2.innerHTML = state.counter2;
$state.innerHTML = JSON.stringify(state);
};
// get initial state
let state = counters(undefined, { type: 'INIT' });
// update dom
update(state);
$counter1.onclick = function (e) {
const action = { type: 'INCREMENT' };
// update state with reducer
state = counters(state, action);
// update dom
update(state);
};
$counter2.onclick = function (e) {
const action = { type: 'INCREMENT' };
// update state with reducer
state = counters(state, action);
// update dom
update(state);
};
And it’s exactly what is possible with the standard combineReducers
function from redux
library. But be aware that as we use the same counter
reducer for counter1 and counter2, they react the same way and counter’s values will be the same too.
Combine a set of reducers into an indexed storage
Let’s imagine that we want a couple of such state structures. We must add an id
parameter into the action to be able to determine which one of the states we want to change.
// the reducer itself
// the state is:
//
// [number]
const counter = (state = 1, action) => {
switch (action.type) {
case 'INCREMENT':
return state + 1;
default:
return state;
}
};
const counters = (state = {}, action) => {
// it makes no sense without an id parameter, so we shouldn't react on that
if (!action.id) {
return state;
}
// we should just proxy the action to the child reducer
return Object.assign({}, state, {
[action.id]: counter(state[action.id], action),
});
};
// get elements
const $counter1 = document.getElementById('counter1');
const $counter2 = document.getElementById('counter2');
const $state = document.getElementById('state');
const update = (state) => {
$counter1.innerHTML = state.counter1 || 1;
$counter2.innerHTML = state.counter2 || 1;
$state.innerHTML = JSON.stringify(state);
};
// get initial state
let state = counters(undefined, { type: 'INIT', id: 'counter1' });
state = counters(state, { type: 'INIT', id: 'counter2' });
// update dom
update(state);
$counter1.onclick = function (e) {
const action = { type: 'INCREMENT', id: 'counter1' };
// update state with reducer
state = counters(state, action);
// update dom
update(state);
};
$counter2.onclick = function (e) {
const action = { type: 'INCREMENT', id: 'counter2' };
// update state with reducer
state = counters(state, action);
// update dom
update(state);
};
Let the power of pure functions be with you.