Skip to content

Voronar/redux-multireducer

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

9 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Redux multireducer concept (RU)

The problem

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 use combineReducers 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.

The solution

To solve this problem we need a specific action types for the specialized version of our reducer function.

FP solution

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.

OOP solution

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.

Example (list view React application)

Redux list module

Redux list module is a directory that includes redux module class and required module instances.

src/redux/modules/list/List.js - Redux list module class

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 your 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 Redux module class and reuse it making so many instances as we need.

src/redux/modules/reducer.js - main 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 list view component

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 - React list view component using

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;

Conclusion

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.

Releases

No releases published

Packages

No packages published