最近又重新开始做起React的网站来,在做的时候想添加骨架屏,之前添加的Skeleton骨架屏有些不够好用。因为每次请求的时候,都要设置Action并且修改reducer里面的isLoading的值,重写一个界面之后又再写一遍类似的代码。所以说很多人都说React冗余复杂,可能就是复杂在这些上面吧。
于是我搜索了一下,发现了一个很好用的方法,自己用了一下觉得真的很好用,就翻译过来。
以下是原文。
目的: 使用单独的reducer存储所有的isFetching标志,而不是每个reducer都存储isFetching而污染了reducer。
相信,每个人使用React作为前端,并将Redux用于状态管理的时候,并且每当应用都有一定复杂程度的时候,我们可能会遇到如下代码。
// todo/actionCreators.js
export const getTodos = (dispatch) => () => {
dispatch({ type: 'GET_TODOS_REQUEST' });
return fetch('/api/todos')
.then((todos) => dispatch({ type: 'GET_TODOS_SUCCESS', payload: todos })
.catch((error) => dispatch({ type: 'GET_TODOS_FAILURE', payload: error, error: true });
};
// todo/reducer.js
const initialState = { todos: [] };
export const todoReducer = (state = initialState, action) => {
switch(action.type) {
//我用的是isLoading
case 'GET_TODOS_REQUEST':
return { ...state, isFetching: true };
case 'GET_TODOS_SUCCESS':
return { ...state, isFetching: false, todos: action.payload };
case 'GET_TODOS_FAILURE':
return { ...state, isFetching: false, errorMessage: action.payload.message };
default:
return state;
}
};
当您的应用程序需要更多的API调用时,此代码才能正常工作。并且,将近一半的代码都是我们要处理的 isFetching / errorMessage 的代码。(写重复的代码难免会有些疲惫 )
加载Loading Reducer
在StashAway中,我们通过创建一个减少负载的方法来解决此问题,该方法存储所有API请求状态:
// api/loadingReducer.js
const loadingReducer = (state = {}, action) => {
const { type } = action;
const matches = /(.*)_(REQUEST|SUCCESS|FAILURE)/.exec(type);
// 如果不是 *_REQUEST / *_SUCCESS / *_FAILURE actions, 我们就将它们忽略
if (!matches) return state;
const [, requestName, requestState] = matches;
return {
...state,
// 存储当前是否正在发生请求
// 例如:当收到GET_TODOS_REQUEST的时候,isFetching为true
// 当收到GET_TODOS_SUCCESS / GET_TODOS_FAILURE的时候,isFetching为false
[requestName]: requestState === 'REQUEST',
};
};
然后我们可以通过组件调用选择器访问那些加载状态。 原作者使用lodash,下面有原生JS的做法。
// api/selectors.js
import _ from 'lodash';
export const createLoadingSelector = (actions) => (state) => {
// 仅在未加载所有action时返回true
return _(actions)
.some((action) => _.get(state, `api.loading.${action}`));
};
// components/todos/index.js
import { connect } from 'react-redux';
import Todos from './Todos';
import { createLoadingSelector } from '../../redux/api/selectors';
// GET_TODOS_REQUEST时,显示正在加载。
const loadingSelector = createLoadingSelector(['GET_TODOS']);
const mapStateToProps = (state) => ({ isFetching: loadingSelector(state) });
export default connect(mapStateToProps)(Todos);
另一种不用lodash的方法
// api/selectors.js
import _ from 'lodash';
export const createLoadingSelector = actions => state =>
actions.some(action => state.loading[action]);
//actions.some(action => state.get("loading")[action]); 如果用immutable
由于我们不需要维护任何isFetching标志,因此我们的reducer只需要关心存储数据,所以代码更加简单:
// todo/reducer.js
const initialState = { todos: [] };
export const todoReducer = (state = initialState, action) => {
switch(action.type) {
case 'GET_TODOS_SUCCESS':
return { ...state, todos: action.payload };
default:
return state;
}
};
这样基本上就好了,虽然在这个测试代码中,显得没有那么简化,但是当您的应用程序要求您向超过20个API发送请求时,这就会让您的生活变得更加轻松一些了。
我们可以合并API调用吗?
当在构建一个真实的应用程序时,我们可能需要结合API调用(例如:当getUser和getTodos请求都成功时,才显示代办事项页面)。这种方法很简单:
// components/todos/index.js
import { connect } from 'react-redux';
import Todos from './Todos';
import { createLoadingSelector } from '../../redux/api/selectors';
// 当 GET_TODOS_REQUEST, GET_USER_REQUEST 中的任意一个处于active时,显示正在加载
const loadingSelector = createLoadingSelector(['GET_TODOS', 'GET_USER']);
const mapStateToProps = (state) => ({ isFetching: loadingSelector(state) });
export default connect(mapStateToProps)(Todos);
那,错误信息处理呢?
处理API错误消息与处理加载标志类似,除了在选择要显示的消息时:
// api/errorReducer.js
export const errorReducer = (state = {}, action) => {
const { type, payload } = action;
const matches = /(.*)_(REQUEST|FAILURE)/.exec(type);
// 如果不是 *_REQUEST / *_FAILURE action, 我们就将其忽略
if (!matches) return state;
const [, requestName, requestState] = matches;
return {
...state,
// 存储 errorMessage
// 例如:当接收到 GET_TODOS_FAILURE 时存储 errorMessage
// 否则在收到 GET_TODOS_REQUEST 时清除 errorMessage
[requestName]: requestState === 'FAILURE' ? payload.message : '',
};
};
// api/selectors.js
import _ from 'lodash';
export const createErrorMessageSelector = (actions) => (state) => {
// 返回操作的第一条错误消息
// *我们假设当任何请求失败时,
// 需要多个API调用,我们显示第一个错误
return _(actions)
.map((action) => _.get(state, `api.error.${action}`))
.compact()
.first() || '';
};
当然,错误信息处理同样可以不用lodash
// api/selectors.js
export const createErrorMessageSelector = actions => (state) => {
const errors = actions.map(action => state.error[action]);
if (errors && errors[0]) {
return errors[0];
}
return '';
};
就像编程中许多事物一样,有很多方式处理React / Redux。这只是我的首选方式。
我们一直都在努力寻找改善的方法!
原文完。
注:在使用loadingReducer时,用combineReducer将loadingReducer进行管理。这里引入之后,通过React Developer Tools可以查看Redux state中的变化。
// store/reducer.js
import { combineReducers } from "redux-immutable";
const reducer = combineReducers({
loading: loadingReducer
});
export default reducer;
在加载请求的时候,执行REQUEST action后,再发请求。
// /components/todos/store/actionCreators.js
import axios from "axios";
const setTodos= (payload) => ({
type: "GET_TODOS_SUCCESS",
payload
});
const getTodosRequest = () => ({
type: "GET_TODOS_REQUEST"
});
export const getTodos = () => {
return (dispatch) => {
dispatch(getTodosRequest()); // 这里正在请求
axios.get("/api/todos").then((res)=>{
dispatch(setTodos(res.data.payload));
}).catch((e)=>{
console.log(e);
});
}
};
大概就是这些了,希望这些能够简化您的Redux代码。