Joining Reducers
Occasionally it is necessary for a state to have multiple reductions applied to it. Typically this occurs when the reducers involved represent fundamentally different operational types.
An example should clarify this concept.
Let's say we have a widget that contains x/y properties:
{
widget: {
x: 100,
y: 200
}
}
The individual x/y properties are nicely managed by the standard Redux.combineReducers function:
import * as Redux from 'redux';
import x from '../appReducer/x';
import y from '../appReducer/y';
const contentReducer =
Redux.combineReducers({
x,
y
});
export default function widget(widget={}, action) {
return contentReducer.reduce(nextState, action);
}
However, what happens if the widget can take on a null value (say for example, it only exists when it is being edited)?
In this case, we have more work to do:
- we need to apply the editor open/close logic, and
- conditionally manage it's content (because Redux.combineReducers cannot operate on null state).
One way to accomplish this is through the following procedural logic:
import * as Redux from 'redux';
import x from '../appReducer/x';
import y from '../appReducer/y';
const contentReducer =
Redux.combineReducers({
x,
y
});
export default function widget(widget=null, action) {
// FIRST: determine content shape (i.e. {} or null)
let nextState = widget;
switch (action.type) {
case 'widget.edit':
nextState = action.widget;
break;
case 'widget.edit.close':
nextState = null;
break;
default:
nextState = widget;
}
// SECOND: maintain individual x/y fields
// ONLY when widget has content (i.e. is being edited)
if (nextState !== null) {
nextState = contentReducer(nextState, action);
}
// are we done yet? ... that was painful!!
return nextState;
}
A more elegant solution can be accomplished by employing reducer
composition, eliminating the procedural code completely. We have
already discussed reducerHash()
and conditionalReducer()
.
A third utility, the joinReducers()
function, combines two or
more reducers logically executing each in sequence.
The following snippet, is equivalent to the one above:
import * as Redux from 'redux';
import * as AstxReduxUtil from 'astx-redux-util';
import x from '../appReducer/x';
import y from '../appReducer/y';
export default AstxReduxUtil.joinReducers(
// FIRST: determine content shape (i.e. {} or null)
AstxReduxUtil.reducerHash({
"widget.edit": (widget, action) => action.widget,
"widget.edit.close": (widget, action) => null
}),
AstxReduxUtil.conditionalReducer(
// SECOND: maintain individual x/y fields
// ONLY when widget has content (i.e. is being edited)
(widget, action, originalReducerState) => widget !== null,
Redux.combineReducers({
x,
y
})),
null); // initialState
Here the joinReducers combines multiple reducers together as one.
The first reducer (reducerHash) interprets the
'widget.edit
/'widget.edit.close'
action type, providing object content or not (i.e. null).The second reducer (conditionalReducer) conditionally invokes the third reducer (Redux.combineReducers), only when the state has content (i.e. non-null).
Reducer composition provides a more elegant solution that is purely functional in nature.