KDE BLOG

バイブス

bindActionCreators を使うと何がいいのか実際コードを書いて理解する

前回の記事で触れた bindActionCreators ですが、今まで使ったことがないので簡単なカウンターアプリを作って、実際に使ってみて一体何がいいのかを確認してみました。

カウンターアプリ ver.1

よくあるやつです。

f:id:jinseirestart:20191117053125g:plain

コード(ファイル名押下でコード展開)

ディレクトリ構成

f:id:jinseirestart:20191117054853p:plain

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

f:id:jinseirestart:20191117052739g:plain

改修箇所は下記になります。

▼ ./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 となります。

f:id:jinseirestart:20191117051353p:plain

mapDispatchToProps 周りを修正します。

▼ ./containers/Counter.js

+function mapDispatchToProps (dispatch) {
+  return {
+    dispatch,
+    ...bindActionCreators({
+      increment,
+      decrement,
+      reset,
+    }, dispatch)
+  }
+}

export default connect(
  mapStateToProps,
-  {
-    increment,
-    decrement,
-    reset,
-  }
+  mapDispatchToProps
)(Counter);

このようにすると、dispatch が渡すことができているのが確認できます。

f:id:jinseirestart:20191117045310p:plain

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をラップする関数を書かずに済む

といったことがちゃんと理解できました。