Redux multireducer concept (RU)
When we want to reuse our single reducer function for multiple reducer instances we face a problem. Redux creator write:
As an example, let's say that we want to track multiple counters in our application, named A, B, and C. We define our initial
counter
reducer, and we usecombineReducers
to set up our state:
function counter(state = 0, action) {
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
}
const rootReducer = combineReducers({
counterA : counter,
counterB : counter,
counterC : counter
});
Unfortunately, this setup has a problem. Because
combineReducers
will call each slice reducer with the same action, dispatching{type : 'INCREMENT'}
will actually cause all three counter values to be incremented, not just one of them.
To solve this problem we need a specific
action types for the specialized version of our reducer function.
Dan offers a solution from a world of functional programming - higher-order reducer. He wraps the reducer with a higher-order function (HOF) and specify an action type this suffix/prefix passing it from HOF. The same approach (he specifies action object with special meta-key
) use Erik Rasmussen in his library.
I approach more or less the same solution but without wrappers, suffixes/prefixes, meta-keys, etc. I highlighted specific
word in a solution section not without a reason. What if we do an action type REALY unique? Greeting, Symbol
. From MDN:
Every symbol value returned from Symbol() is unique. A symbol value may be used as an identifier for object properties; this is the data type's only purpose.
Perfect choice, is not it? And why we need object-oriented programming? OOP help us to optimize the code organization and make our action types unique. Redux ingredients (or Redux module) organization (reducer, constants, action creators) was inspired by a modular redux approach from all the same developer Erik Rasmussen.
Let's try the approach in a list view React application example (working example included in this repository, just clone it, npm i
and npm run start
).
️️ NOTICE ️️
Symbol
constants impose some restrictions for several of Redux's defining features, such as time travel debugging, and recording and replaying actions. More information read there. 😉 But this problem is simple to resolve.
Redux list
module is a directory that includes redux module class and required module instances.
import * as services from './../../../api/services';
const initialState = {
list: [],
};
function getListReducer(state, action) {
return {
...state,
list: action.payload.list,
};
}
function removeItemReducer(state, action) {
const { payload } = action;
const list = state.list.filter((item, i) => i !== payload.index);
return {
...state,
list,
};
}
export default class List {
constructor() {
// action types constants
this.GET_LIST = Symbol('GET_LIST');
this.REMOVE_ITEM = Symbol('REMOVE_ITEM');
}
getList = (serviceName) => {
return async (dispatch) => {
const list = await services[serviceName].get();
dispatch({
type: this.GET_LIST,
payload: {
list,
serviceName,
},
});
};
}
removeItem = (index) => {
return (dispatch) => {
dispatch({
type: this.REMOVE_ITEM,
payload: {
index,
},
});
};
}
reducer = (state = initialState, action) => {
switch (action.type) {
case this.GET_LIST:
return getListReducer(state, action);
case this.REMOVE_ITEM:
return removeItemReducer(state, action);
default:
return state;
}
}
}
⚠️ ️ IMPORTANT⚠️ ️ Action creators and reducer must be class instance methods, not prototype methods otherwise you will loose yourthis
.
// Redux list module class
import List from './List';
export default {
users: new List(),
posts: new List(),
};
Just create Redux module class and reuse it making so many instances as we need.
import { combineReducers } from 'redux';
// required Redux module instances
import list from './list/index';
export default combineReducers({
users: list.users.reducer,
posts: list.posts.reducer,
});
import * as React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from "redux";
// Redux module instances
import list from './../redux/modules/list';
class ListView extends React.Component {
componentWillMount() {
this.props.getList(this.props.serviceName);
}
render() {
return (
<div>
<h1>{this.props.serviceName}</h1>
<ul>
{this.props.list.map((item, i) =>
<span key={i}>
<li style={{ width: 100 }}>
{item}
<button style={{ float: 'right' }} onClick={() => this.props.removeItem(i)}>x</button>
</li>
</span>)
}
</ul>
<button onClick={() => this.props.getList(this.props.serviceName)}>Update</button>
</div>
);
}
}
const mapStateToProps = (state, { serviceName }) => ({
...state[serviceName],
});
const mapDispatchToProps = (dispatch, { serviceName }) => ({
...bindActionCreators({ ...list[serviceName]}, dispatch),
});
export default connect(mapStateToProps, mapDispatchToProps)(ListView);
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
import ListView from './components/ListView';
class App extends Component {
render() {
return (
<div className="App">
<div className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h2>Try to Redux multireducer</h2>
</div>
<ListView serviceName="users" />
<ListView serviceName="posts" />
</div>
);
}
}
export default App;
This way using modern JavaScript you can do your Redux module more reusable. I will be glad to listen your suggestions and critique in repository Issue section.