Как стать автором
Обновить

Современный JavaScript или как сделать ваш Redux-модуль готовым к переиспользованию

Время на прочтение5 мин
Количество просмотров13K


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



Концепция мультиредьюсера 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-приложения для отображения списков (рабочий пример c интеграцией с redux devtools chrome extension находится в репозитории этой документации, просто скопируйте репозиторий, и запустите пару комманд npm i и npm run start).


️️ ПРЕДУПРЕЖДЕНИЕ ️️ Symbol константы накладывают некоторые ограничения на такие возможности Redux, как отладка с перемещением во времени, запись и воспроизведение экшенов. Больше информации читайте тут. Но эта проблема легко решается.

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


Redux list модуль


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


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


src/redux/modules/list/List.js
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;
    }
  }
}

src/redux/modules/list/List.devtools.ready.js
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;
    }
  }
}

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

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


// Redux list module class
import List from './List';

export default {
  users: new List(),
  posts: new List(),
};

Просто создаём класс Redux-модуля и переиспользуем его, делаю столько экземпляров, сколько требуется.


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


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


src/App.jsx
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;

Заключение


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

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
А как вы делаете мультиредьюсеры?
33.33% Дублирую редьюсеры18
48.15% Редьюсер высшего порядка26
1.85% Библиотека miltireducer1
22.22% Свой вариант (укажите в комментарии)12
Проголосовали 54 пользователя. Воздержался 71 пользователь.
Теги:
Хабы:
+12
Комментарии18

Публикации

Истории

Работа

Ближайшие события

Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн
Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн