bindActionCreators を使うと何がいいのか実際コードを書いて理解する
前回の記事で触れた bindActionCreators
ですが、今まで使ったことがないので簡単なカウンターアプリを作って、実際に使ってみて一体何がいいのかを確認してみました。
カウンターアプリ ver.1
よくあるやつです。
コード(ファイル名押下でコード展開)
ディレクトリ構成
redux
./redux/actionTypes.js
export const INCREMENT = 'increment'; export const DECREMENT = 'decrement'; export const RESET = 'reset';
./redux/actions.js
import { INCREMENT, DECREMENT, RESET } from "./actionTypes"; // Action Creators export const increment = () => ({ type: INCREMENT }); export const decrement = () => ({ type: DECREMENT }); export const reset = () => ({ type: RESET });
./redux/reducers.js
import { INCREMENT, DECREMENT, RESET } from "./actionTypes"; const initialState = { count: 0 }; export default function coreReducer (state = initialState, action) { switch (action.type) { case INCREMENT: { return { ...state, count: state.count + 1 }; } case DECREMENT: { return { ...state, count: state.count - 1 }; } case RESET: { return { ...state, count: 0 }; } default: return state; } }
./redux/reducers.js
import { createStore } from "redux"; import coreReducer from './reducers'; export default createStore(coreReducer);
container component
./containers/Counter.js
import { connect } from 'react-redux'; import Counter from "../components/Counter"; import { increment, decrement, reset } from "../redux/actions"; function mapStateToProps (state) { return { count: state.count } } export default connect( mapStateToProps, { increment, decrement, reset } )(Counter);
presentational component
./components/Counter.jsx
import React from 'react'; import Button from "./Button"; import NumberText from './NumberText'; function Counter({ increment, decrement, reset, count }) { return ( <div> <NumberText num={count} /> <Button onClick={decrement} text="-" /> <Button onClick={increment} text="+" /> <Button onClick={reset} text="reset" /> </div> ); } export default Counter;
./components/Button.jsx
import React from 'react'; export default function Button ({ onClick, text }) { return ( <button onClick={onClick}>{text}</button> ); }
./components/NumberText.jsx
import React from "react"; export default function NumberText ({ num }) { return ( <div><strong> {num} </strong></div> ) }
エントリーポイント
./index.js
import React from 'react'; import { render } from 'react-dom'; import { Provider } from "react-redux"; import store from "./redux/store"; import Counter from "./containers/Counter"; render( <Provider store={store}> <Counter /> </Provider>, document.getElementById('root') );
さて、今回の bindActionCreators
を使う所としては connect
の第二引数としての masDispatchToProps
の所になるので ./containers/Counter.js
内になります。
まずは上記で記述したように、bindActionCreators を使わないパターンをみてみます。
Action Creators だけのオブジェクトを渡す
export default connect( mapStateToProps, { increment, decrement, reset } )(Counter);
こちらのパターンは、公式ドキュメントでも推奨されている方法です。
一番シンプルな方法だと思います。
dispatch を引数に取った関数を定義
function mapDispatchToProps (dispatch) { return { increment: () => dispatch(increment()), decrement: () => dispatch(decrement()), reset: () => dispatch(reset()), } } export default connect( mapStateToProps, mapDispatchToProps )(Counter);
少し前は割とこの方法が定番だったかもしれません。
dispachする処理をラップした関数を作るだけなのですが、上記のオブジェクトショートハンドのやり方と比べると面倒です。
bindActionCreatorsを使った方法
import { bindActionCreators } from "redux"; // 略 function mapDispatchToProps (dispatch) { return bindActionCreators({ increment, decrement, reset, }, dispatch); } export default connect( mapStateToProps, mapDispatchToProps )(Counter);
bindActionCreators
を使うと、上記の2つの方法のあいだくらいの記述量で書くことができます。
こうした記述がなぜできるのかをいうと、前回の記事にも書いてありますが、公式ドキュメントにこう説明があります。
bindActionCreators
は、値が Action Creator であるオブジェクトを、同じキーを持つオブジェクトに変換するが、すべての Action Creator は dispatch 呼び出しにラップされているため、直接呼び出すことができる。
さて、こう見比べると、最初の「Action Creators だけのオブジェクトを渡す」方法が一番コード量が少ないので、あえて bindActionCreators を使う意味がないように感じます。
「Action Creators に引数を渡す必要が出てくれば違うかもしれない」と思ったので、カウンターアプリを改修して試してみます。
カウンターアプリ ver.2
改修箇所は下記になります。
▼ ./redux/actions.js
- export const increment = () => ({ type: INCREMENT }); + export const increment = (count) => ({ type: INCREMENT, payload: { count } }); - export const decrement = () => ({ type: DECREMENT }); +export const decrement = (count) => ({ type: DECREMENT, payload: { count } });
▼ ./redux/reducers.js
case INCREMENT: { return { ...state, - count: state.count + 1 + count: state.count + action.payload.count }; } case DECREMENT: { return { ...state, - count: state.count - 1 + count: state.count - action.payload.count }; }
▼ ./index.js
render( <Provider store={store}> - <Counter /> + <Counter range={5} /> </Provider>, document.getElementById('root') );
▼ ./components/Counter.jsx
- function Counter({ increment, decrement, reset, count }) { + function Counter({ increment, decrement, reset, count, range }) { + if (range <= 0) { + return ( + <p>Please specify 1 or more for the 'range' prop.</p> + ); + } + const ary = Array(range).fill(range); return ( <div> <NumberText num={count} /> - <Button onClick={decrement} text="-" /> - <Button onClick={increment} text="+" /> + {/* decrement */} + {ary.map((item, index) => + <Button + key={`decrement_${item - index}`} + onClick={decrement.bind(null, item - index)} + text={`-${item - index}`} + /> + )} + {/* increment */} + {ary.map((item, index) => + <Button + key={`increment_${item - index}`} + onClick={increment.bind(null, item - index)} + text={`+${item - index}`} + /> + ).reverse()} <Button onClick={reset} text="reset" /> </div> ); }
最後に mapDispatchToProps の箇所を確認してみます。
ver1と同様、Action Creatorsだけのオブジェクトをconnect関数の第二引数に渡している形です。
export default connect( mapStateToProps, { increment, decrement, reset } )(Counter);
これで実行してみると…
問題なく想定通り動作しました!
もう一度公式ドキュメントを確認してみると、
関数ではなく Action Creator で満たされたオブジェクトを渡すと、connect は自動的に bindActionCreators を内部的に呼び出す。
と記載があります。
では結局、bindActionCreators を使うと何がいいのかというと、connectされたコンポーネントに dispatch
もpropsとして渡したいときに、他のメソッドを簡潔に書けるということになります。
先ほどの connect されている ./components/Counter.jsx
に dispatch を受け渡すようにしてみます。
▼ ./components/Counter.jsx
- function Counter({ increment, decrement, reset, count, range }) { + function Counter({ increment, decrement, reset, count, range, dispatch }) { + console.log('dispatch :', dispatch);
上記だけの変更だと dispatch は渡されずに undefined
となります。
mapDispatchToProps 周りを修正します。
▼ ./containers/Counter.js
+function mapDispatchToProps (dispatch) { + return { + dispatch, + ...bindActionCreators({ + increment, + decrement, + reset, + }, dispatch) + } +} export default connect( mapStateToProps, - { - increment, - decrement, - reset, - } + mapDispatchToProps )(Counter);
このようにすると、dispatch が渡すことができているのが確認できます。
bindActionCreators を使わない場合は、下記のようになります。
function mapDispatchToProps (dispatch) { return { dispatch, - ...bindActionCreators({ - increment, - decrement, - reset, - }, dispatch) + increment: (...args) => dispatch(increment(...args)), + decrement: (...args) => dispatch(decrement(...args)), + reset: (...args) => dispatch(reset(...args)) } }
どちらがいいというとやはり、bindActionCreators
を使った方が楽です。
まとめ
- mapDispatchToProps の代わりに、Action Creators で満たしたオブジェクトを渡すと内部的に bindActionCreators が実行される
- bindActionCreators を使うと、mapDispatchToProps をカスタマイズしたり、
dispatch
を明示的に props として渡したいときに わざわざ他のdispatchをラップする関数を書かずに済む
といったことがちゃんと理解できました。