打入react&redux(3)- reducer的拆分与组合
25 July 2016

react机制下一切皆组件,没有页面的概念。但是在使用redux划分state时还是有页面倾向的,说页面可能并不准确,更准确的是state是按功能集合划分的。

reducer中state的划分

state总体上是按页面划分的,譬如现在有个列表页和详情页,还有一些用于多个组件的公用信息如个人信息(或者嵌入多个页面不同地方的组件,总之是公用信息)。我会这么划分:

在各页面的reducer中维护各自的state

  • list页的reducer:

      // list.js
      import * as ActionTypes from '../constants/ActionTypes';
      const initialState = {
          totalPage: 1,
          pageIndex: 0, 
          list:[]
      };
    	
      export default function handler(state = initialState, action) {
          switch (action.type) {
              case ActionTypes.APPEND_LIST:
                  //...
              default:
                  return state;
          }
      }
    
  • detail页的reducer:

      //detail.js
      import * as ActionTypes from '../constants/ActionTypes';
      const initialState = {
          title: 1,
          author: 0, 
          content:'',
          comment:[]
      };
    	
      export default function handler(state = initialState, action) {
          switch (action.type) {
              case ActionTypes.APPEND_COMMENT:
                  //...
              default:
                  return state;
          }
      }
    
  • userInfo组件的reducer:

      //userInfo.js
      import * as ActionTypes from '../constants/ActionTypes';
      const initialState = {
          userName:'',
          userId:0,
          avator:''
      };
    	
      export default function handler(state = initialState, action) {
          switch (action.type) {
              case ActionTypes.GET_INFO:
                  //...
              default:
                  return state;
          }
      }
    

使用combineReducers将state合并

// reducers.js
import { combineReducers } from 'redux';

import list from '../reducers/list';
import detail from '../reducers/detail';
import userInfo from '../reducers/userInfo';

export default combineReducers({
	list,
	detail,
    userInfo
});

connect redux

封装一个connectRedux函数将state的分支按需分发给组件。

import React from 'react';
import { connect } from 'react-redux';

/* components */
var List = require('ROOT_PATH/pages/ListView');
var Detail = require('ROOT_PATH/pages/DetalView');

var connectRedux = function(component, model = [], actions = null) {
  return connect(
    (state) => {
      let needState = {};
      if(model.length){
        model.map((item)=>{
          Object.assign(needState, state[item]);
        })
      }else{
        needState = state;
      }
      return needState;
    }
  )(component);
}

List = connectRedux(List, ['list','userInfo']);
Detail = connectRedux(Detail, ['detail','userInfo']);

这样List组件能访问到:

const {totalPage, pageIndex, list, userName, userId, avator} = this.props

Detail组件同理。

action的处理

项目中会遇到这种情景。用户的某个操作可能导致需要更新属于state不同部分的数据。依旧拿上文中的数据举例,例如用户列表包括自己,假想有个弹窗可以更改自己头像,用户的更改将导致state.list[0].avatorstate.userInfo.avator都被更新,我们该如何派发action更新2部分数据呢?

不能同时dispatch多个action

我以前的做法是同时dispatch2个action,每个action更新state的不同部分,原则是以数据源区分action。随着深入了解,这是很不科学的,dispatch action引发state更新,state更新引发组件rerender,这个过程是个”类异步”(不是真正的异步)过程,你没办法保证哪个action先更新state触发rerender。有可能一前一后,也有可能,a先要被更新,但是组件的isBatchingUpdates为false,处于等待更新状态,这时b的更新需求来了,也等待了一阵直到isBatchingUpdates为true,最后2个state部分引发的组件更新被合并更新了,也就是值触发一个rerender。这种不确定性是可怕的,尤其是当2个state之间有依赖或者逻辑关联时,那就会出bug了。如果action中有异步操作,顺序就更不能保证了。

通过action.type分发

这种情况比较靠谱的方案是通过相同的action.type执行2部分的state更新。

ListView中:

function changeAvator(avator,index){
    let myInfo = Object.assign({},this.props.list[index],{
        avator:avator
    });
    this.props.dispatch(changeUserInfo(userInfo,index));
}

list页的action:

// list.js
export function changeUserInfo(userInfo,index){
    return {
        type: CHANGE_MY_INFO,
        userInfo: userInfo,
        index:index
    }
}

list页的reducer:

import * as ActionTypes from '../constants/ActionTypes';
const initialState = {
    totalPage: 1,
    pageIndex: 0, 
    list:[]
};

export default function handler(state = initialState, action) {
    let list = state.list.concat();
    switch (action.type) {
        case ActionTypes.CHANGE_MY_INFO:
            list[action.index] = action.userInfo;
            return Object.assign({},state,{
                list:list
            })

        default:
            return state;
    }
}

userInfo的reducer:

import * as ActionTypes from '../constants/ActionTypes';
const initialState = {
    userName:'',
    userId:0,
    avator:''
};

export default function handler(state = initialState, action) {
    switch (action.type) {
        case ActionTypes.CHANGE_MY_INFO:
            return Object.assign({},state,{
                userName:action.userInfo.userName,
                userId:action.userInfo.userId,
                avator:action.userInfo.avator
            })

        default:
            return state;
    }
}

可以看到通过在不同reducer中对同一action.type处理,更新各自管理的数据,可以达到需求。

以上内容只是我在项目中使用的方式,不一定是最好的,特别是通过action.type分发state的不同部分。如果你有更好的方法,欢迎留言指导。