If we want to reuse our reducer function for several instances of the final reducer, then we are faced with a problem.
Redux creator writes about this:
For example, wecombineReducers
tocombineReducers
our state ofcombineReducers
:
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.combineReducers
will be thecombineReducers
willcombineReducers
.
types of actions for a certain version of our function reducer.Dan offers a solution from the world of functional programming - a higher order reducer. He wraps the reducer with a higher order function ( FVP ) and names the type of action with an additional suffix / prefix that is forwarded through the FVP . A similar approach (he specializes an action object using a special -
) uses Erik Rasmussen in his library .
I suggest a more or less similar approach, but without wrappers, suffixes / prefixes, meta keys, and the like. In the solution section, I highlighted the word not without reason. What if we take and make the type of action REALLY unique? Meet the
Symbol
. MDN clipping:
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 only purpose.
Ideal, is not it? And where does object-oriented programming ? OOP allows you to organize our code in the most optimal way and make our types of action games unique. The way the organization of the Redux-ingredients (or Redux-module) was inspired by the modular Redux of the same Erik Rasmussen .
Let's try to apply this approach using the example of a React application to display lists (a working example with integration with redux devtools chrome extension is in the repository of this documentation, just copy the repository, and run a couple of npm i
and npm run start
commands).
️️ WARNING ️️ Symbol
constants impose some restrictions on Redux features such as time-shifted debugging, action recording and playback. Read more here . But this problem is easily solved .
list
moduleRedux list
module - directory in which the Redux-class of the module and the required instances of this module are located.
src/redux/modules/list/List.js
- Redux-class of the list module 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; } } }
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, }; } function actionType(name) { return { type: name, metaType: Symbol(name), }; } export default class List { constructor(prefix) { this.GET_LIST = actionType(`${prefix}/GET_LIST`); this.REMOVE_ITEM = actionType(`${prefix}/REMOVE_ITEM`); } getList = (serviceName) => { return async (dispatch) => { const list = await services[serviceName].get(); const { type, metaType } = this.GET_LIST; dispatch({ payload: { list, serviceName, }, type, metaType, }); }; } removeItem = (index) => { return (dispatch) => { const { type, metaType } = this.REMOVE_ITEM; dispatch({ payload: { index, }, type, metaType, }); }; } reducer = (state = initialState, action) => { switch (action.metaType) { case this.GET_LIST.metaType: return getListReducer(state, action); case this.REMOVE_ITEM.metaType: return removeItemReducer(state, action); default: return state; } } }
️️ IMPORTANT Action generators and reducer should be methods of the class instance, not its prototype, otherwise you will lose this
.
src/redux/modules/list/index.js
- Redux module instances // Redux list module class import List from './List'; export default { users: new List(), posts: new List(), };
Just create a class of Redux-module and reuse it, make as many instances as necessary.
src/redux/modules/reducer.js
- Master Reducer import { combineReducers } from 'redux'; // required Redux module instances import list from './list/index'; export default combineReducers({ users: list.users.reducer, posts: list.posts.reducer, });
src/components/ListView.js
- React-component for displaying lists 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);
src/App.jsx
- Using a React Component to Display Lists 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;
Thus, using modern JavaScript you can make your Redux-module more convenient for reuse. I would be glad to hear criticism and suggestions in the Issue section of the repository of this documentation.
Source: https://habr.com/ru/post/323164/
All Articles