[Из песочницы] Redux-symbiote — пишем действия и редьюсеры почти без боли
React-redux замечательная штука. При правильном использовании архитектура приложения эффективна, а структура проекта и легко читаемая. Но как и в любом решении есть свои особенности.
Описание действий и редьюсеров одна из таких особенностей. Классическая реализация двух этих сущностей в коде довольно трудоемкое занятие.
Боль классической реализации
Простой пример:
// actionTypes.js
// описываем типы действий
export const POPUP_OPEN_START = 'POPUP_OPEN_START ';
export const POPUP_OPEN_PENDING = 'POPUP_OPEN_PENDING ';
export const POPUP_OPEN_SUCCESS = 'POPUP_OPEN_SUCCESS ';
export const POPUP_OPEN_FAIL = 'POPUP_OPEN_FAIL';
export const POPUP_CLOSE_START = 'POPUP_CLOSE_START ';
export const POPUP_CLOSE_PENDING = 'POPUP_CLOSE_PENDING ';
export const POPUP_CLOSE_SUCCESS = 'POPUP_CLOSE_SUCCESS ';
export const POPUP_CLOSE_FAIL = 'POPUP_CLOSE_FAIL';
// actions.js
// описываем сами действия
import {
POPUP_OPEN_START,
POPUP_OPEN_PENDING,
POPUP_OPEN_SUCCESS,
POPUP_OPEN_FAIL,
POPUP_CLOSE_START,
POPUP_CLOSE_PENDING,
POPUP_CLOSE_SUCCESS,
POPUP_CLOSE_FAIL
} from './actionTypes';
export function popupOpenStart(name) {
return {
type: POPUP_OPEN_START,
payload: {
name
},
}
}
export function popupOpenPending(name) {
return {
type: POPUP_OPEN_PENDING,
payload: {
name
},
}
}
export function popupOpenFail(error) {
return {
type: POPUP_OPEN_FAIL,
payload: {
error,
},
}
}
export function popupOpenSuccess(name, data) {
return {
type: POPUP_OPEN_SUCCESS,
payload: {
name,
data
},
}
}
export function popupCloseStart(name) {
return {
type: POPUP_CLOSE_START,
payload: {
name
},
}
}
export function popupClosePending(name) {
return {
type: POPUP_CLOSE_PENDING,
payload: {
name
},
}
}
export function popupCloseFail(error) {
return {
type: POPUP_CLOSE_FAIL,
payload: {
error,
},
}
}
export function popupCloseSuccess(name) {
return {
type: POPUP_CLOSE_SUCCESS,
payload: {
name
},
}
}
// reducers.js
// реализуем редьюсеры
import {
POPUP_OPEN_START,
POPUP_OPEN_PENDING,
POPUP_OPEN_SUCCESS,
POPUP_OPEN_FAIL,
POPUP_CLOSE_START,
POPUP_CLOSE_PENDING,
POPUP_CLOSE_SUCCESS,
POPUP_CLOSE_FAIL
} from './actionTypes';
const initialState = {
opened: []
};
export function popupReducer(state = initialState, action) {
switch (action.type) {
case POPUP_OPEN_START:
case POPUP_OPEN_PENDING:
case POPUP_CLOSE_START:
case POPUP_CLOSE_PENDING:
return {
...state,
error: null,
loading: true
};
case POPUP_OPEN_SUCCESS :
return {
...state,
loading: false,
opened: [
...(state.opened || []).filter(x => x.name !== action.payload.name),
{
...action.payload
}
]
};
case POPUP_OPEN_FAIL:
return {
...state,
loading: false,
error: action.payload.error
};
case POPUP_CLOSE_SUCCESS:
return {
...state,
loading: false,
opened: [
...state.opened.filter(x => x.name !== name)
]
};
case POPUP_CLOSE_FAIL:
return {
...state,
loading: false,
error: action.payload.error
};
}
return state;
}
На выходе имеем 3 файла и как минимум следующие проблемы:
- «раздувание» кода при простом добавлении новой цепочки действий
- избыточный импорт констант действий
- чтение имен констант действий (индивидуально)
Оптимизация
Данный пример можно улучшить с помощью redux-actions.
import { createActions, handleActions, combineActions } from 'redux-actions'
export const actions = createActions({
popups: {
open: {
start: () => ({ loading: true }),
pending: () => ({ loading: true }),
fail: (error) => ({ loading: false, error }),
success: (name, data) => ({ loading: false, name, data }),
},
close: {
start: () => ({ loading: true }),
pending: () => ({ loading: true }),
fail: (error) => ({ loading: false, error }),
success: (name) => ({ loading: false, name }),
},
},
}).popups
const initialState = {
opened: []
};
export const accountsReducer = handleActions({
[
combineActions(
actions.open.start,
actions.open.pending,
actions.open.success,
actions.open.fail,
actions.close.start,
actions.close.pending,
actions.close.success,
actions.close.fail
)
]: (state, { payload: { loading } }) => ({ ...state, loading }),
[combineActions(actions.open.fail, actions.close.fail)]: (state, { payload: { error } }) => ({ ...state, error }),
[actions.open.success]: (state, { payload: { name, data } }) => ({
...state,
error: null,
opened:
[
...(state.opened || []).filter(x => x.name !== name),
{
name, data
}
]
}),
[actions.close.success]: (state, { payload: { name } }) => ({
...state,
error: null,
opened:
[
...state.opened.filter(x => x.name !== name)
]
})
}, initialState)
Уже намного лучше, но совершенству нет предела.
Лечим боль
В поисках более оптимального решения наткнулись на комментарий LestaD habr.com/ru/post/350850/#comment_10706454 и решили попробовать redux-symbiote.
Это позволило убрать лишние сущности и уменьшить количество кода.
Пример выше стал выглядеть вот так:
// symbiotes/popups.js
import { createSymbiote } from 'redux-symbiote';
export const initState = {
opened: []
};
export const { actions, reducer } = createSymbiote(initialState, {
popups: {
open: {
start: state => ({ ...state, error: null }),
pending: state => ({ ...state }),
success: (state, { name, data } = {}) => ({
...state,
opened: [
...(state.opened || []).filter(x => x.name !== name),
{
name,
data
})
]
}),
fail: (state, { error } = {}) => ({ ...state, error })
},
close: {
start: state => ({ ...state, error: null }),
pending: state => ({ ...state }),
success: (state, { name } = {}) => ({
...state,
opened: [
...state.opened.filter(x => x.name !== name)
]
}),
fail: (state, { error } = {}) => ({ ...state, error })
}
}
});
// пример вызова
import {
actions
} from './symbiotes/popups';
// ...
export default connect(
mapStateToProps,
dispatch => ({
onClick: () => {
dispatch(actions.open.start({ name: PopupNames.Info }));
}
})
)(FooComponent);
Из плюсов имеем:
- все в одном файле
- меньше кода
- структурированное представление действий
Из минусов:
- IDE не всегда предлагает подсказки
- сложно искать действие в коде
- сложно переименовать действие
Не смотря на минусы данный модуль успешно используется в наших проектах.
Спасибо LestaD за хорошую работу.