banner
他山之石

他山之石

手摸手撸一個簡單的Redux(五)

Redux 使用 CombineReducer 來組合多個 reducer 函數。

image

之前我們已經完成了 redux 和 react-redux 的大部分功能,本文將結合之前的 To do list 項目來完善我們編寫的 redux 和 react-redux,主要是實現 combineReducer 以及 mapDiapatch 的默認參數和讓 mapDispatch 支持 function 參數。

本文完整代碼請查看 Github:https://github.com/YanYuanFE/redux-app

// clone repo
git clone https://github.com/YanYuanFE/redux-app.git


cd redux-app

// checkout branch
git checkout part-7

// install
npm install

// start
npm start

combineReducer#

隨著應用變得越來越複雜,我們會將 reducer 根據業務進行拆分,拆分後的 reducer 函數負責獨立管理 state 的一部分。

Redux 為我們提供了 combineReducer 這個輔助函數,用於將我們拆分後的多個 reducer 根據自身的鍵值進行組合成一個新的 object,成為新的 reducer,然後對這個 reducer 調用 createStore 方法。

combineReducer 合併後的 reducer 可以調用各個子 reducer,並且把返回的結果合併成一個 state 對象。由 combineReducer 返回的 state 對象,會將傳入的每個 reducer 返回的 state 按其傳遞給 combineReducer 時對應的 key 進行命名。

通常在項目中,我們會為每個單獨的 reducer 單獨創建 js 文件,在 reducer 中為每個 reducer 進行命名並導出,然後在 reducer 的入口文件中導入,通過 Redux 的 combineReducer 為每個 reducer 進行命名不同的 key 來控制不同的 state 的 key 的命名。
下面回顧一下在之前 Todo list 項目中對 combineReducer 的使用。
reducers/index.js:

import { combineReducers } from 'redux';
import todos from './todos';
import visibilityFilter from './visibilityFilter';

const todoApp = combineReducers({
  todos,
  visibilityFilter
});

export default todoApp;

combineReducers ({todos, visibilityFilter}) 這裡使用了 ES6 的對象簡寫語法,這與
combineReducers ({todos: todos, visibilityFilter: visibilityFilter}) 是等價的。
此處需要注意,combineReducer 中傳入對象的 key 與 redux 中存儲的 state 同名。

下面來實現一個簡單的 combineReducer。
在 src/redux.js 中:

export function combineReducers(reducers) {
  const finalReducerKeys = Object.keys(reducers);

  return (state = {}, action) => {
    let nextState = {};
    finalReducerKeys.forEach((key) => {
      const reducer = reducers[key];
      const prevStateForKey = state[key];
      const nextStateForKey = reducer(prevStateForKey, action);
      nextState[key] = nextStateForKey;
    });

    return nextState;
  }

}

可以看到,combineReducer 是一個高階函數,返回一個 function,首先通過 Object.keys 獲取到由 reducers
的 key 組成的數組 finalReducerKeys,然後返回一個 function,這個 function 是一個組合後的 reducer 函數,
接收 state 和 action 參數,state 默認為空對象,在 function 內部,定義 nextState 為空對象,然後對
finalReducerKeys 進行遍歷,通過數組的 key 獲取到每一個 reducer,然後為 reducer 傳入前一個 state 和
action,返回新的 state 並加入到 nextState 的對象中,最後返回新的 state。
我們還可以用更加簡潔的代碼來實現:

export function combineReducers(reducers) {
  
  const finalReducerKeys = Object.keys(reducers);

  return (state = {}, action) => {

    return finalReducerKeys.reduce((ret, item) => {
       ret[item] = reducers[item](state[item], action);
       return ret;
    }, {});
  }

}

上述代碼中,使用 reduce 對一個空對象進行累加操作,對數組每一項進行計算並返回一個新的對象,代碼更加簡潔。

新的 To do List#

現在我們將之前使用 react-redux 實現的 To do List 項目使用自己實現的 react-redux,修改 containers 文件夾下
的 AddTodo.js、FilterLink.js、VisibleTodoList.js,將引用的 react-redux 修改為自己編寫的
react-redux,如下:

import { connect } from '../react-redux'

然後運行項目,開始報錯:

image

emm。

好吧,查看報錯信息,發現 containers/FilterLink.js 文件中:

import { connect } from '../react-redux'
import { setVisibilityFilter } from '../actions'
import Link from '../components/Link'

const mapStateToProps = (state, ownProps) => ({
  active: ownProps.filter === state.visibilityFilter
})

const mapDispatchToProps = (dispatch, ownProps) => ({
  onClick: () => {
    dispatch(setVisibilityFilter(ownProps.filter))
  }
})

const FilterLink = connect(
  mapStateToProps,
  mapDispatchToProps
)(Link);

export default FilterLink

在 mapStateToProps 和 mapDispatchToProps 都使用了 ownProps 參數,但是再看下我們自己的 react-redux 中
connect 的參數。

update() {
  const { store } = this.context;
  const stateProps = mapStateToProps(store.getState());
  const dispatchProps = bindActionCreators(mapDispatchToProps, store.dispatch);

  this.setState({
    props: {
      ...this.state.props,
      ...stateProps,
      ...dispatchProps,
    }
  })
}

使用 mapStateToProps 並沒有傳入 props,修改如下:

const stateProps = mapStateToProps(store.getState(), this.props);

然後,沒有報錯了,但是頁面好像有點問題,下面的篩選按鈕沒有顯示出來。

image

使用 React 開發者工具查看,是因為 props 沒有傳遞下去。修改上述代碼,將 ConectComponent
中的 this.props 也傳遞下去。

this.setState({
  props: {
    ...this.state.props,
    ...stateProps,
    ...dispatchProps,
    ...this.props,
  }
})

再查看界面,顯示好了,下面嘗試新增一個 to do。輸入提交後又報錯了。

image

查看報錯信息:

TypeError: dispatch is not a function

原來是 AddTodo 組件的 props 中沒有 dispatch 方法,分析如下,使用 connect 將 AddTodo 組件與 Redux 進行連接,
但是 connect 中並沒有傳遞參數,mapDispatch 參數被默認定義為空對象,這裡應該默認定義為一個 dispatch 方法,
修改如下:

export const connect = (mapStateToProps = state => state, mapDispatchToProps) => (WrapComponent) => {
  return class ConectComponent extends React.Component {
    static contextTypes = {
      store: PropTypes.object
    }

    constructor(props, context) {
      super(props, context);
      this.state = {
        props: {}
      }
    }
    componentDidMount() {
      const { store } = this.context;
      store.subscribe(() => this.update());
      this.update();
    }
    update() {
      const { store } = this.context;
      const stateProps = mapStateToProps(store.getState(), this.props);
      if (!mapDispatchToProps) {
       
          mapDispatchToProps = { dispatch: store.dispatch}
      }
     

      this.setState({
        props: {
          ...this.state.props,
          ...stateProps,
          ...dispatchProps,
          ...this.props,
        }
      })
    }
    render() {
      return <WrapComponent {...this.state.props}/>
    }
  }
}

這裡主要是去掉了 mapDispatch 的默認參數,在 update 函數中對其進行判斷是否為空,為空則傳遞一個對象,對象包含
一個 dispatch 方法。
再次嘗試添加 todo,添加成功,但是出現了兩條數據,可能是連續觸發了兩次 dispatch,點擊篩選按鈕試試,報錯了。

image
查看報錯信息:

TypeError: _onClick is not a function

components/Link.js 中的 onClick props 沒有傳遞進來,查看 onClick 方法定義的地方:
在 containers/FilterLink.js 中,在 mapDispatch 中進行了定義:

const mapDispatchToProps = (dispatch, ownProps) => ({
  onClick: () => {
    dispatch(setVisibilityFilter(ownProps.filter))
  }
})

在我們實現的 connect 中,mapDispatch 只支持傳遞對象參數,下面進行修改,讓其支持傳遞函數。
修改 update 方法:

let dispatchProps;
if (typeof mapDispatchToProps === 'function') {
    dispatchProps = mapDispatchToProps(store.dispatch, this.props);
} else {
    dispatchProps = bindActionCreators(mapDispatchToProps, store.dispatch);
}

上述代碼的作用主要是對 mapDispatch 進行處理,判斷其類型是否為 function,如果為 function 則執行一下,
傳入 store.dispatch 和 this.props 參數,返回一個對象,然後賦值到 dispatchProps 上。
然後嘗試點擊篩選按鈕,沒有報錯,功能正常,但是添加 todo 還是會出現兩條數據,經過調試發現是因為又執行了一次 bindActionCreator,嘗試將 mapDispatch 的默認值修改為一個函數,如下:

if (!mapDispatchToProps) {
  mapDispatchToProps = (dispatch) => ({dispatch})
}
let dispatchProps;
if (typeof mapDispatchToProps === 'function') {
    dispatchProps = mapDispatchToProps(store.dispatch, this.props);
} else {
    dispatchProps = bindActionCreators(mapDispatchToProps, store.dispatch);
}

這樣,下面對 mapDispatch 進行判斷後,會返回一個對象,包含 dispatch 方法。
重新添加 todo,成功,數據正確。如下圖。
image

細心的你發現了嗎?在右邊的控制台一直會出現警告,大概意思是我們的組件 props 參數應該是一些數值但是實際上
是 undefined,因為在組件內部定義了 PropTypes。

Link.propTypes = {
  active: PropTypes.bool.isRequired,
  children: PropTypes.node.isRequired,
  onClick: PropTypes.func.isRequired
}

export default Link
Link.propTypes = {
  active: PropTypes.bool.isRequired,
  children: PropTypes.node.isRequired,
  onClick: PropTypes.func.isRequired
}

export default Link

問題應該出現在我們的 connect 方法中,在 connect 方法中,最終 render 返回包裹的組件,並傳遞 this.state.props
到包裹的組件,但是 this.state.props 初始為空,只有在 componentDidMount 才會調用 this.update 方法更新 state,
而組件執行是先 render 再執行 componentDidMount,故第一次 render 時所有的 props 都是 undefined,造成報錯。
我們可以將 componentDidMount 修改為 componentWillMount,componentWillMount 在 render 之前執行,可以避免
這個報錯。修改如下:

componentWillMount() {
  const { store } = this.context;
  store.subscribe(() => this.update());
  this.update();
}

最終效果如下,沒有報錯信息,嘗試添加 todo 和修改,功能都正常。

image

參考:

https://github.com/ecmadao/Coding-Guide/tree/master/Notes/React/Redux

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。