Пишем redux по SOLID

В данном посте мы коснемся написания action’ов и reducer’а. Для начала рассмотрим типичный ‘flow’, в котором мы выполняем следующие операции (далее переработаем все так, чтобы наш код отвечал принципам SOLID).

1. создаем файл с константами (здесь мы сохраняем названия типов action’ов)

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. создаем файл, где описываем action’ы (здесь мы делаем запрос на получение учеток пользователей, и пагинация). Также в примере был использован redux-thunk (далее мы откажемся от подобных зависимостей):

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. мы пишем 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. настраиваем store (применяем 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. подключаем компонент к redux

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

подключаем кнопки пагинации к redux

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

Проблема: наш редьюсер представляет собой одну большую инструкцию switch, следовательно при добавлении нового action’а, или изменения его поведения нам необходимо изменять наш редьюсер, что нарушает принципы SOlid (принцип единственной ответственности и принцип открытости/закрытости).

Решение: нам поможет полиморфизм. Добавим к каждому action’у метод execute, который будет применять обновление и возвращать обновленный state. Тогда наш reducer примет вид

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

теперь при добавлении нового action’а нам не понадобиться изменять reducer, и он не превратиться в огромного монстра.

Далее откажемся от redux-thunk и перепишем action’ы

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

переходим к подключенному компоненту, action которого асинхронный (его придется совсем слегка подкорректировать)

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

и перейдем к самим action’ам и добавим им метод execute

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}) }) 

Внимание: свойство type обязательное (если его не добавить, будет выброшено исключение). Но для нас оно не имеет вообще никакого значения. Именно поэтому у нас отпадает потребность в отдельном файле с перечислением типов action’ов.

P.S.: В данной статье мы применили принципы SRP и OCP, полиморфизм, отказались от сторонней библиотеки и сделали наш код более чистым и поддерживаемым.

FavoriteLoadingДобавить в избранное
Posted in Без рубрики

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *