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代码。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,753评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,668评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,090评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,010评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,054评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,806评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,484评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,380评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,873评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,021评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,158评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,838评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,499评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,044评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,159评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,449评论 3 374
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,136评论 2 356

推荐阅读更多精彩内容