Современный JavaScript или как сделать ваш редьюсер лучше

habralogo.jpg

Если мы хотим переиспользовать нашу функцию-редьюсер для нескольких экземпляров конечного редьюсера, то мы сталкиваемся с проблемой.



Концепция мультиредьюсера Redux (EN)

Проблема


Создатель Redux вот что пишет по этому поводу:


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.

Решение


Для решения этой проблемы нам нужны уникальные типы экшенов для определенной версии нашей функции-редьюсера.


Решение с использованием ФП


Дэн предлагает решение из мира функционального программирования — редьюсер высшего порядка. Он оборачивает редьюсер с помощью функции высшего порядка (ФВП) и именует тип экшена с помощью дополнительного суффикса/префикса, пробрасываемого через ФВП. Похожий подход (он специализирует объект-экшен с помощью специального мета-ключа) использует Erik Rasmussen в своей библиотеке.


Решение с использованием ООП


Я предлагаю более-менее схожий подход, но без обёрток, суффиксов/преффиксов, мета-ключей и тому подобного. В секции решения я выделил слово специфичные не без причины. Что, если мы возьмём и сделаем тип экшена ДЕЙСТВИТЕЛЬНО уникальным? Встречайте, Symbol. Вырезка с сайта 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.

Идеальный вариант, не правда ли? А причём тут объектно-ориентированное программирование? ООП позволяет организовать наш код наиболее оптимальным образом и сделать наши типы экшенов уникальными. Способ организация Redux-ингридиентов (или Redux-модуля) был навеян модульным Redux всё того же Erik Rasmussen.
Давайте попробуем применить этот подход на примере React-приложения для отображения списков (рабочий пример находится в репозитории этой документации, просто скопируйте репозиторий, и запустите пару комманд npm i и npm run start).


Пример (React-приложение для отображения списков)


Redux list модуль


Redux list модуль — директория в которой расположены Redux-класс модуля и требуемые экземпляры этого модуля.


src/redux/modules/list/List.js — Redux-класс модуля списка


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 async (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;
    }
  }
}

️️ ВАЖНО ️️ Генераторы экшенов и редьюсер должны быть методами экземпляра класса, а не его прототипа, в противном случае вы потеряете this.

src/redux/modules/list/index.js — Экземпляры модуля Redux


// 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 — Главный редьюсер


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-компонент для отображения списков


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 (
      

{this.props.serviceName}

    {this.props.list.map((item, i) =>
  • {item}
  • ) }
); } } const mapStateToProps = (state, { serviceName }) => ({ ...state[serviceName], }); const mapDispatchToProps = (dispatch, { serviceName }) => ({ ...bindActionCreators({ ...list[serviceName]}, dispatch), }); export default connect(mapStateToProps, mapDispatchToProps)(ListView);

src/App.jsx — Использование React-компонента для отображения списков


import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
import ListView from './components/ListView';

class App extends Component {
  render() {
    return (
      
logo

Try to Redux multireducer

); } } export default App;

Заключение


Таким образом, используя современный JavaScript вы можете сделать ваш Redux-модуль более удобным для переиспользования. Буду рад услышать критику и предложения в секции Issue репозитория этой документации.

Комментарии (0)

© Habrahabr.ru