📜 ⬆️ ⬇️

Redux-symbiote - we write actions and reducers almost without pain

React-redux is a great thing. When used properly, the application architecture is efficient, and the project structure is easy to read. But as in any decision has its own characteristics.

The description of actions and reductors is one of such features. The classic implementation of these two entities in code is quite time-consuming.

The pain of a classic implementation


A simple example:

// 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; } 

At the exit, we have 3 files and at least the following problems:
')

Optimization


This example can be improved by using 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) 

Already much better, but there is no limit to perfection.

Treat pain


In search of a more optimal solution, I stumbled upon a LestaD comment habr.com/en/en/post/350850/#comment_10706454 and decided to try redux-symbiote .
This made it possible to remove unnecessary entities and reduce the amount of code.

The example above began to look like this:

 // 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); 

Of the benefits we have:


Of the minuses:


Despite the disadvantages, this module is successfully used in our projects.

Thank you LestaD for good work.

Source: https://habr.com/ru/post/442346/


All Articles