Git Product home page Git Product logo

redux-multireducer's Introduction

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.

redux-multireducer's People

Contributors

voronar avatar

Watchers

 avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.