KDE BLOG

バイブス

【React Redux】mapDispatchToProps の公式ドキュメント訳

前回の mapStateToProp に続いて mapDispatchToProps の公式ドキュメントについても google 翻訳の力を借りて目を通したので、残しておきます。

react-redux.js.org


概要

connect に渡される2番目の引数として、mapDispatchToProps は Action を store に dispatch するために使用される。

dispatch は Redux store の機能で、store.dispatch を呼び出して、Action を dispatch する。これが stateの変更をトリガーする唯一の方法 である。

React Redux を使用すると、コンポーネントが store に直接アクセスすることはなく、 connect が自動的にアクセスする。
React Redux には、コンポーネントに Action を dispatch させる2つの方法がある。

  • デフォルトでは、connectされたコンポーネントprops.dispatch を受け取り、Action 自体を dispatch できる。
  • connect は、mapDispatchToProps という引数を受け入れることができる。
    これにより、呼び出されたときに dispatch する関数を作成し、それらの関数をコンポーネントとしてコンポーネントとして渡すことができる。

mapDispatchToProps 関数は通常、略して mapDispatch と呼ばれますが、実際に使用される変数名は任意である。

dispatchするためのアプローチ

デフォルト:Prop としての dispatch

connect 関数に2番目の引数を指定しない場合、コンポーネントはデフォルトで dispatch を受け取る。

例)

// 引数なし
connect()(MyComponent);

// ↑と同じ意味
connect(null, null)(MyComponent);

// または2番目の引数なし
connect(mapStateToProps)(MyComponent);

この方法でコンポーネントを connect すると、コンポーネントprops.dispatch を受け取る。これを使用して、 store に Action を dispatch できる。

function Counter({ count, dispatch }) {
  return (
    <div>
      <button onClick={() => dispatch({ type: 'DECREMENT' })}>-</button>
      <span>{count}</span>
      <button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button>
      <button onClick={() => dispatch({ type: 'RESET' })}>reset</button>
    </div>
  )
}

mapDispatchToProps の提供

mapDispatchToProps を提供すると、コンポーネントが dispatch する必要がある Action を指定できる。
これにより、Action を dispatch する機能を props として提供できる。
したがって、下記のような書き方をすることができる。

// props.dispatch(() => increment());

// 上記の代わりにこう書ける
props.increment();

このように行う理由はいくつかある。

より宣言的である

まず、dispatch ロジックを関数にカプセル化すると、実装がより宣言的になる。
Action を dispatch し、Redux store にデータフローを処理させることが,何をするかではなく、動作を実装する方法である。

良い例は、ボタンがクリックされたときに Action を dispath することである。
ボタンを直接 connect することは、おそらく概念的に意味がなく、ボタン参照を dispatch することもできない。

// ボタンは "dispatch" に意識する必要がある
<button onClick={() => dispatch({ type: "SOMETHING" })} />

// ボタンは "dispatch" を意識する必要がない
<button onClick={doSomething} />

すべての Action Creator を Action を dispatch する関数でラップすると、コンポーネントdispatch が必要がなくなる。
したがって独自の mapDispatchToProps を定義すると、connect されたコンポーネントdispatch を受け取ることがなくなる。

(store 未接続の)子コンポーネントにロジックを dispatch する Action を渡す

さらに、Action を dispatch する関数を子(ストアと未接続の可能性が高い)コンポーネントに渡すこともできる。
これにより、より多くのコンポーネントが Action を dispatch できるが、それらのコンポーネントは Redux を認識することがない

// toggleTodo を子コンポーネントに渡す
// Todoが toggleTodo アクションをディスパッチできるようにする
const TodoList = ({ todos, toggleTodo }) => (
  <div>
    {todos.map(todo => (
      <Todo todo={todo} onClick={toggleTodo} />
    ))}
  </div>
)

これが、React Redux の connect の機能である。
Redux store と通信するロジックをカプセル化し、心配する必要がない。
そして、これはあなたがあなたの実装で完全に最大限に活用すべきものである。

mapDispatchToProps の2つの形式

mapDispatchToProps パラメーターには2つの形式がある。
関数の形式ではさらにカスタマイズができるが、オブジェクト形式は扱いやすい。

  • 関数形式:より多くのカスタマイズを許可し、dispatch にアクセスし、オプションでownProps を受け取ることができる
  • オブジェクトの短縮形式:より宣言的で使いやすい

※ 何らかの方法で dispatch の動作をカスタマイズする必要がない限り、オブジェクト形式の mapDispatchToProps を勧める。

関数として mapDispatchToProps を定義する

mapDispatchToProps を関数として定義すると、コンポーネントが受け取る関数、および Action を dispatch する方法をカスタマイズする際の柔軟性が最大になる。
dispatch および ownProps にアクセスできる。

引数

  1. dispatch
  2. ownProps (オプショナル)
dispatch

mapDispatchToProps 関数は、最初の引数として dispatch を使用して呼び出される。
通常、この機能を利用するには、内部で dispatch() を呼び出す新しい関数を返し、プレーンな Action オブジェクトか、Action Creator の結果を dispatch に渡す。

const mapDispatchToProps = dispatch => {
  return {
    // プレーンな Action の dispatch
    increment: () => dispatch({ type: 'INCREMENT' }),
    decrement: () => dispatch({ type: 'DECREMENT' }),
    reset: () => dispatch({ type: 'RESET' })
  };
};

また、Action Creator に引数を渡すこともできる。

const mapDispatchToProps = dispatch => {
  return {
    // 引数を明示的に渡す
    onClick: event => dispatch(trackClick(event)),

    // 暗黙的に引数を渡す
    onReceiveImpressions: (...impressions) =>
      dispatch(trackImpressions(impressions))
  };
};
ownProps (オプショナル)

mapDispatchToProps 関数が2つのパラメーターを取るように宣言されている場合、最初のパラメーターとして dispatch を、2番目のパラメーターとして connectされたコンポーネントに渡された props を使用して呼び出され、connectされたコンポーネントが新しい props を受け取るたびに再起動される。

つまり、コンポーネントの再レンダリング時に Action Dispatcher に新しい props を再バインドする代わりに、コンポーネントの props が変更されたときにそうすることができる。

// コンポーネントの再レンダリングにバインドする
<button onClick={() => this.props.toggleTodo(this.props.todoId)} />

// "props" の変更にバインドする
const mapDispatchToProps = (dispatch, ownProps) => {
  toggleTodo: () => dispatch(toggleTodo(ownProps.todoId))
}

戻り値

mapDispatchToProps 関数はプレーンな object を返す必要がある。

  • オブジェクトの各フィールドは、独自のコンポーネントの個別の props になる。値は通常、実行時に Action を dispatch する関数である必要がある。
  • dispatch 内で Action Creator(プレーンオブジェクトアクションとは反対)を使用する場合、フィールド key に Action Creator と同じ名前を付けるだけの規則である:
const increment = () => ({ type: 'INCREMENT' })
const decrement = () => ({ type: 'DECREMENT' })
const reset = () => ({ type: 'RESET' })

const mapDispatchToProps = dispatch => {
  return {
    // Action Creator から返された Action を dispatch する
    increment: () => dispatch(increment()),
    decrement: () => dispatch(decrement()),
    reset: () => dispatch(reset())
  }
}

mapDispatchToProps 関数の戻り値は、props として connect されたコンポーネントにマージされる。それらを直接呼び出して、Action を dispatch できる。

function Counter({ count, increment, decrement, reset }) {
  return (
    <div>
      <button onClick={decrement}>-</button>
      <span>{count}</span>
      <button onClick={increment}>+</button>
      <button onClick={reset}>reset</button>
    </div>
  )
}

(Counter サンプルの全コード:https://codesandbox.io/s/yv6kqo1yw9

bindActionCreators を使用して mapDispatchToProps 関数を定義する

これらの関数を手動でラップするのは退屈なので、Redux はそれを単純化する関数を提供している。

bindActionCreators は、値が Action Creator であるオブジェクトを、同じキーを持つオブジェクトに変換するが、すべての Action Creator は dispatch 呼び出しにラップされているため、直接呼び出すことができる。
bindActionCreatorsのReduxドキュメントを参照

bindActionCreators は2つの引数を受け取る。

  1. 関数(Action Creator)または オブジェクト(各フィールドは Action Creator
  2. dispatch

bindActionCreators によって生成されたラッパー関数は、すべての引数を自動的に転送するため、手動で行う必要はない。

import { bindActionCreators } from 'redux'

const increment = () => ({ type: 'INCREMENT' })
const decrement = () => ({ type: 'DECREMENT' })
const reset = () => ({ type: 'RESET' })

// Action Creator のバインド
// 戻り値: (...args) => dispatch(increment(...args))
const boundIncrement = bindActionCreators(increment, dispatch);

// Action Creator で満たされたオブジェクトをバインド
const boundActionCreators = bindActionCreators(
  { increment, decrement, reset },
  dispatch
);
// 戻り値:
// {
//   increment: (...args) => dispatch(increment(...args)),
//   decrement: (...args) => dispatch(decrement(...args)),
//   reset: (...args) => dispatch(reset(...args)),
// }

mapDispatchToProps 関数で bindActionCreators を使用する:

import { bindActionCreators } from 'redux'
// ...

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

// component receives props.increment, props.decrement, props.reset
connect(null, mapDispatchToProps)(Counter);

手動で dispatch する

mapDispatchToProps 引数が指定されている場合、コンポーネントはデフォルトの dispatch を受信しなくなる。
mapDispatchToProps の戻り値に手動で追加して戻すことができるが、ほとんどの場合これを行う必要はない。

import { bindActionCreators } from 'redux'
// ...

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

オブジェクトとして mapDispatchToProps を定義する

Reactコンポーネントで Redux Action を dispatch するためのセットアップは、非常に似たプロセスに従うことがわかった:
Action Creator を定義し、(…args) => dispatch (actionCreator(…args)) のような別の関数でラップし、そのラッパー関数をコンポーネントの props として渡す。

これは非常に一般的であるため、connect は mapDispatchToProps 引数の「オブジェクトショートハンド」形式をサポートしている。
関数ではなく Action Creator で満たされたオブジェクトを渡すと、connect は自動的に bindActionCreators を内部的に呼び出す。

dispatch の動作をカスタマイズする特別な理由がない限り、mapDispatchToProps の「オブジェクトショートハンド」形式を常に使用することを勧める。

留意:

  • mapDispatchToProps オブジェクトの各フィールドは、Action Creator であると想定されている
  • コンポーネントは props として dispatch を受け取らなくなる
// React Reduxはこれを自動的に行う
dispatch => bindActionCreators(mapDispatchToProps, dispatch);

したがって、mapDispatchToProps は単純に次のようになる。

const mapDispatchToProps = {
  increment,
  decrement,
  reset
}

変数の実際の名前はユーザー次第であるため、actionCreators などの名前を付けるか、connect の呼び出しでオブジェクトをインラインで定義することもできる。

import { increment, decrement, reset } from './counterActions';

const actionCreators = {
  increment,
  decrement,
  reset
};

export default connect(mapState, actionCreators)(Counter);
// or
export default connect(
  mapState,
  { increment, decrement, reset }
)(Counter);

一般的な問題

なぜコンポーネントdispatch を受け取らないのか?

TypeError: this.props.dispatch is not a function

これは、this.props.dispatch を呼び出そうとしたときに発生する一般的なエラーだが、dispatch はコンポーネントに注入されない。
次の場合にのみ、ディスパッチがコンポーネントに注入される。

1. mapDispatchToProps を提供しない

デフォルトの mapDispatchToProps は、単純に dispatch => ({dispatch}) である。
mapDispatchToProps を提供しない場合、このように dispatch が提供される。下記と同じ働き。

connect(mapStateToProps)(Component);
2.カスタマイズされた mapDispatchToProps 関数の戻り値に明示的に dispatch を含める
const mapDispatchToProps = dispatch => ({
  increment: () => dispatch(increment()),
  decrement: () => dispatch(decrement()),
  reset: () => dispatch(reset()),
  dispatch
});

bindActionCreators を使う場合:

import { bindActionCreators } from 'redux';

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

https://github.com/reduxjs/react-redux/issues/255 を見ると、mapDispatchToProps を指定するときにコンポーネントにdispatch を提供するかどうかについての議論がある( #255 に対する Dan Abramov の応答)。
現在の実装の意図をさらに理解するためにそれらを読むことができる。

通常 mapDispatchToProps を使用して、ラップするコンポーネントからReduxを非表示にする。
dispatch を常に公開するということは、プレゼンテーションコンポーネントから Redux を抽象化する方法がないことを意味している。

Reduxで mapStateToProps なしで mapDispatchToProps を使用できるか?

できる。undefined または null を渡すことにより、第1引数をスキップできる。
コンポーネントは store に subscribe せず、mapDispatchToProps で定義された dispatch プロパティを受け取る。

connect(null, mapDispatchToProps)(MyComponent)

store.dispatch を呼び出すことはできるか?

store の明示的なインポートであれ、context を介したアクセスであれ、React コンポーネントで store と直接対話するのはアンチパターンである(詳細については Redux FAQ entry on store setupを参照)。


所感

  • mapDispatchToProps に Action Creator だけのオブジェクトをショートハンドで渡すことで props として受け取って dispatch できることは知らなかったので、今回知れてよかった。
  • コンポーネントから dispatch を参照できないようにする理由が知れてよかった。
  • bindActionCreators は使用したことがないので次回試してみる。
    • 2020.11.19追記:試した結果を記事にしました。kde.hateblo.jp