banner
他山之石

他山之石

手を取り合ってシンプルなReduxを作る(五)

Redux は CombineReducer を使用して複数の reducer 関数を組み合わせます。

image

以前、私たちは redux と react-redux のほとんどの機能を完成させました。この記事では、以前の To do list プロジェクトを組み合わせて、私たちが作成した redux と react-redux を完成させます。主に combineReducer の実装と mapDispatch のデフォルトパラメータの設定、mapDispatch が function パラメータをサポートするようにします。

この記事の完全なコードは Github でご覧ください:https://github.com/YanYuanFE/redux-app

// リポジトリをクローン
git clone https://github.com/YanYuanFE/redux-app.git


cd redux-app

// ブランチをチェックアウト
git checkout part-7

// インストール
npm install

// スタート
npm start

combineReducer#

アプリケーションがますます複雑になるにつれて、私たちはビジネスに基づいて reducer を分割します。分割された reducer 関数は、state の一部を独立して管理します。

Redux は combineReducer という補助関数を提供しており、分割された複数の reducer を自身のキーに基づいて組み合わせて新しいオブジェクトを作成し、新しい reducer として createStore メソッドを呼び出します。

combineReducer でマージされた reducer は、各子 reducer を呼び出し、返された結果を 1 つの state オブジェクトにマージします。combineReducer から返される state オブジェクトは、渡された各 reducer から返された state を、combineReducer に渡すときの対応する key に基づいて命名します。

通常、プロジェクトでは、各独立した reducer のために個別の js ファイルを作成し、各 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

ええと。

エラーメッセージを確認すると、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 メソッドがないことが原因です。分析すると、AddTodo コンポーネントを Redux に接続するために connect を使用しましたが、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 メソッドを含むオブジェクトを渡すようにしました。
再度 to do を追加してみると、成功しましたが、データが 2 つ表示されました。連続して 2 回 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 に代入します。
次にフィルターボタンをクリックしてみると、エラーは発生せず、機能は正常に動作しましたが、to do を追加するとまだ 2 つのデータが表示されます。デバッグの結果、再度 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 メソッドを含むオブジェクトが返されます。
再度 to do を追加してみると、成功し、データが正しく表示されました。以下の図のように。
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 時にすべての props が undefined であるため、エラーが発生します。
componentDidMount を componentWillMount に変更することで、このエラーを回避できます。以下のように修正します:

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

最終的な効果は以下の通りです。エラーメッセージはなく、to do の追加や変更機能も正常に動作します。

image

参考:

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

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。