Redux は CombineReducer を使用して複数の reducer 関数を組み合わせます。
以前、私たちは 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'
その後、プロジェクトを実行すると、エラーが発生しました:
ええと。
エラーメッセージを確認すると、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);
その後、エラーは発生しませんでしたが、ページに少し問題があるようです。下のフィルターボタンが表示されません。
React 開発者ツールを使用して確認すると、props が渡されていないためです。上記のコードを修正し、ConectComponent 内の this.props も渡すようにします。
this.setState({
props: {
...this.state.props,
...stateProps,
...dispatchProps,
...this.props,
}
})
再度画面を確認すると、表示されました。次に、to do を追加してみると、再びエラーが発生しました。
エラーメッセージを確認すると:
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 がトリガーされた可能性があります。フィルターボタンをクリックしてみると、エラーが発生しました。
エラーメッセージを確認すると:
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 を追加してみると、成功し、データが正しく表示されました。以下の図のように。
注意深いあなたは気づきましたか?右側のコンソールに常に警告が表示されます。おおよそ、私たちのコンポーネントの 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 の追加や変更機能も正常に動作します。
参考:
https://github.com/ecmadao/Coding-Guide/tree/master/Notes/React/Redux