В данном посте мы коснемся написания 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