React / Redux技巧:处理reducer中loading标志的更好方法

最近又重新开始做起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。这只是我的首选方式。

我们一直都在努力寻找改善的方法!

原文完。

源地址,原作者:Sam Aryasa


注:在使用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代码。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容