Writing redux by SOLID

In this post we will touch on writing actions and reducer. To begin with, consider a typical 'flow', in which we perform the following operations (further, we rework everything so that our code meets the SOLID principles).



1. create a file with constants (here we save the names of action types)



export const REQUEST_DATA_PENDING = "REQUEST_DATA_PENDING"; export const REQUEST_DATA_SUCCESS = "REQUEST_DATA_SUCCESS"; export const REQUEST_DATA_FAILED = "REQUEST_DATA_FAILED"; export const PROFILES_PER_PAGE = "PROFILES_PER_PAGE"; export const CURRENT_PAGE = "CURRENT_PAGE";
      
      





2. create a file where we describe the actions (here we make a request for user accounts, and pagination). Also in the example redux-thunk was used (further we will refuse similar dependencies):



 export const requestBigDataAction = () => (dispatch) => { fetchingData(dispatch, BIG_DATA_URL, 50); } export const changeCurrentPAGE = (page) => ({ type: CURRENT_PAGE, payload: page }) function fetchingData(dispatch, url, profilesPerPage) { dispatch({type: REQUEST_DATA_PENDING}); fetch(url) .then((res) => { if(res.status !== 200) { throw new Error (res.status); } else { return res.json(); } }) .then((data) => {dispatch({type: REQUEST_DATA_SUCCESS, payload: data})}) .then(() => dispatch({type: PROFILES_PER_PAGE, payload: profilesPerPage})) .catch((err) => dispatch({type: REQUEST_DATA_FAILED, payload: ` . ${err.message}`})); }
      
      





3. we write reducer



 import { REQUEST_DATA_PENDING, REQUEST_DATA_SUCCESS, REQUEST_DATA_FAILED, PROFILES_PER_PAGE, CURRENT_PAGE } from '../constants/constants'; const initialState = { isPending: false, buffer: [], data: [], error: "", page: 0, profilesPerPage: 0, detailedProfile: {} } export const MainReducer = (state = initialState, action = {}) => { switch(action.type) { case REQUEST_DATA_PENDING: return Object.assign({}, state, {isPending: true}); case REQUEST_DATA_SUCCESS: return Object.assign({}, state, {page : 0, isPending: false, data: action.payload, error: "", detailedProfile: {}, buffer: action.payload}); case REQUEST_DATA_FAILED: return Object.assign({}, initialState, {error: action.payload}); case PROFILES_PER_PAGE: return Object.assign({}, state, {profilesPerPage: action.payload}); case CURRENT_PAGE: return Object.assign({}, state, {page: action.payload}); default: return state; } }
      
      





4. configure the store (use middleware thunkMiddleware)



 import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './App'; import {createStore, applyMiddleware} from 'redux'; import {Provider} from 'react-redux'; import thunkMiddleware from 'redux-thunk'; import {MainReducer} from './reducers/mainReducer'; const store = createStore(MainReducer, applyMiddleware(thunkMiddleware)); ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById('root'));
      
      







5. connect the component to redux

 const mapDispatchToProps = (dispatch)=>{ return { onRequestBigData: (event) =>{ dispatch(requestBigDataAction()); } } };
      
      





connect pagination buttons to redux



 const mapDispatchToProps = (dispatch)=>{ return { onChangePage: (page) =>{ dispatch(changeCurrentPAGE(page)); } } };
      
      





Problem: our reducer is one big switch statement, therefore, when adding a new action, or changing its behavior, we need to change our reducer, which violates the principles of SOlid (the principle of sole responsibility and the principle of openness / closeness).



Solution: polymorphism will help us. Add to each action the execute method, which will apply the update and return the updated state. Then our reducer will take the form



 export const MainReducer = (state = initialState, action) => { if(typeof action.execute === 'function') return action.execute(state); return state; };
      
      





now when adding a new action, we don’t need to change the reducer, and it does not turn into a huge monster.



Next, give up redux-thunk and rewrite the actions



 import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './App'; import {createStore} from 'redux'; import {Provider} from 'react-redux'; // import thunkMiddleware from 'redux-thunk'; import {MainReducer} from './reducers/mainReducer'; const store = createStore(MainReducer); ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById('root'));
      
      





go to the connected component, whose action is asynchronous (it will have to be slightly corrected)



 const mapDispatchToProps = (dispatch)=>{ return { onRequestBigData: (event) =>{ requestBigDataAction(dispatch); }, } };
      
      





and move on to the actions themselves and add the execute method to them



 const type = 'bla-bla'; const requestDataPending = {execute: state => ({...state, isPending: true}), type}; const requestDataSuccess = payload => ({ execute: function (state) { return ({...state, page : 0, isPending: false, data: payload, error: "", detailedProfile: {}, buffer: payload}) }, type}) const profilesPerPageAction = profilesPerPage => ({ execute: state => ({...state, profilesPerPage: profilesPerPage}), type }); const requestDataFailed = errMsg => state => ({...state, error: ` . ${errMsg}`}); function fetchingData(dispatch, url, profilesPerPage) { dispatch(requestDataPending); fetch(url) .then((res) => { if(res.status !== 200) { throw new Error (res.status); } else { return res.json(); } }) .then((data) => {dispatch(requestDataSuccess(data))}) .then(() => dispatch(profilesPerPageAction(profilesPerPage))) .catch((err) => dispatch(requestDataFailed(err.message))); } export const requestBigDataAction = (dispatch) => { fetchingData(dispatch, BIG_DATA_URL, 50); } export const changeCurrentPAGE = page => ({ type, execute: state => ({...state, page}) })
      
      





Note: the type property is required (if you do not add it, an exception will be thrown). But for us it does not matter at all. That is why we no longer need a separate file listing the types of actions.



PS: In this article, we applied the principles of SRP and OCP, polymorphism, abandoned the third-party library and made our code more clean and maintainable.



All Articles