Пишем redux по SOLID

в 9:33, , рубрики: javascript, React, ReactJS, 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, полиморфизм, отказались от сторонней библиотеки и сделали наш код более чистым и поддерживаемым.

Автор: Marat

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js