Если мы хотим переиспользовать нашу функцию-редьюсер для нескольких экземпляров конечного редьюсера, то мы сталкиваемся с проблемой.
Концепция мультиредьюсера 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 usecombineReducers
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-класс модуля списка
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;
}
}
}
️️ ВАЖНО ️️ Генераторы экшенов и редьюсер должны быть методами экземпляра класса, а не его прототипа, в противном случае вы потеряете
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-компонент для отображения списков
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-компонента для отображения списков
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 репозитория этой документации.
Автор: Voronar