KDE BLOG

バイブス

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

React-reduxの公式ドキュメントを久しぶりに見てみると、いろいろと更新されていたので、ちゃんと読みこむことをしています。 今回、connect を使って store とコンポーネントを接続する際に使用する mapStateToProps についての下記ドキュメントを google 翻訳の力で訳したので、自分用にメモ。

react-redux.js.org


概要

connect の最初の引数として、 mapStateToProps は connect されたコンポーネントが必要とする store のデータを一部だけを渡すために使用される。
略して mapState と呼ばれる。

  • store の state が変更するたびに呼ばれる
  • store の state 全体を受け取り、コンポーネントが必要するデータを プレーンな object として返す必要がある

mapStateToProps の定義

関数として定義する。

function mapStateToProps(state, ownProps?) {...}

store に subscribe したくない場合は mapStateToProps の代わりに null or undefined を渡して connect する。(不要な再レンダリングを防げる)

mapStateToProps は、function mapStateToProps(state) {} の関数宣言文でも、 const mapStateToProps = (state) => { } の関数式でも問題ない。

引数

state

mapStateToProps の最初の引数は store の state 全体(store.getState() の呼び出しで返される値と同じ)。
そのため、最初の引数は伝統的に単に state と呼ばれる。引数には任意の名前が可能だが store を呼び出すことは正しくない(「store インスタンス」ではなく「state value」)。

mapStateToPropsは 常に state が引数に渡されて使われる必要がある。

// TodoList.js

function mapStateToProps(state) {
  const { todos } = state;
  return { todoList: todos.allIds };
}

export default connect(mapStateToProps)(TodoList);
ownProps (オプショナル)

コンポーネントが store から データを取得するために、自分の props からデータを必要とする場合、第2引数の ownProps を使って mapStateToPropsを定義できる。
この ownProps には connect で生成されたラッパーコンポーネントのすべての props が含まれる。

// ConnectedTodo.js

function mapStateToProps(state, ownProps) {
  // ...
  const { id, name } = ownProps;
  return {
    // ...
  };
}

export default connect(mapStateToProps)(ConnectedTodo);
// App.js

<ConnectedTodo id={123} name="taro" />

戻り値

mapStateToProps は、コンポーネントが必要とするデータを含む プレーンなobjectを返す 必要がある。

function mapStateToProps(state) {
  return {
    a: 1,
    todos: state.todos
  };
}

// TodoListコンポーネントの props: props.a, props.todos
export default connect(mapStateToProps)(TodoList);

レンダリングのパフォーマンスをさらに制御する必要がある高度なシナリオでは、mapStateToProps は関数を返すこともできる。
その場合、その関数は特定のコンポーネントインスタンスの最終的な mapStateToProps として使用される。これにより、インスタンスごとのメモ化を行うことができる。
しかし、ほとんどのアプリケーションはこれを必要としない。

使用ガイドライン

mapStateToProps に store からのデータを再形成させる

mapStateProps は単に state を分割して返すだけではなく、そのコンポーネントの必要に応じて store データを「再形成」する責任がある。
これには、特定の props 名として値を返すこと、state ツリーの様々な部分からのデータを組み合わせること、およびに、様々な方法で store データを変換することが含まれている。

selectors 関数を使ってデータを抽出および変換する

state ツリーの特定の場所から値を抽出するプロセスをカプセル化するのに役立つ selector 関数の使用を強く勧める。
メモ化された selector 関数は、アプリケーションのパフォーマンスを向上させる上でも重要な役割を果たす。
selector を使用する理由と方法の詳細については、次のセクションおよび「Advanced Usage: Performance」ページを参照)

mapStateToProps 関数は高速でなければならない

store が変更されるたびに connect されているすべてのコンポーネントの mapStateToProps 関数がすべて実行されるため、mapStateToProps 関数はできるだけ高速に実行する必要がある。

「データの再形成」の方法の一部として、mapStateToProps 関数はさまざまな方法でデータを変換する必要がある(配列のフィルタリング、IDの配列の対応するオブジェクトへのマッピング、Immutable.jsオブジェクトからのプレーンJS値の抽出など)。
多くの場合、これらの変換は、変換を実行するためのコストと、結果としてコンポーネントが再レンダリングされるかどうかの両方の点で不利益になる可能性がある。
パフォーマンスが懸念される場合は、入力値が変更された場合にのみこれらの変換が実行されるようにする。

mapStateToProps 関数は純粋で同期的な関数であるべき

redux reducer と同様に、mapStateToProps 関数は常に100%純粋で同期的である必要がある。

シンプルに state(および ownProps)を引数として受け取り、コンポーネントが props として必要とするデータを返す必要がある。
データを取得するための AJAX 呼び出しなどの非同期動作をトリガーするために使用しない。 また、関数を async function として宣言しない。

mapStateToProps とパフォーマンス

戻り値は、コンポーネントが再レンダリングされるかどうかを決める

React Redux は、コンポーネントが必要とするデータが変更されたときにラッパーコンポーネントが正確に再レンダリングされるように shouldComponentUpdate メソッドを内部的に実装する。
デフォルトでは、React Redux は、返されたオブジェクトの各フィールドで === 比較(「浅い等価性」チェック)を使用して、 mapStateToProps から返されたオブジェクトの内容が異なるかどうかを判断する。
いずれかのフィールドが変更された場合、コンポーネントは再レンダリングされ、更新された値を props として受け取ることができる。
同じ参照の変更されたオブジェクトを返すことはよくある間違いで、予期したときにコンポーネントが再レンダリングされない可能性があることに注意すること。

mapStateToProps に接続して store からデータを抽出してラップされたコンポーネントの動作を要約すると以下のようになる。

(state) => stateProps (state, ownProps) => stateProps
mapStateToProps が実行される時 store の state が変更された store の state が変更された
または
ownProps のいずれかのフィールドが異なっている
コンポーネントが再レンダリングされる時 stateProps のいずれかのフィールドが異なっている stateProps のいずれかのフィールドが異なっている
または
ownProps のいずれかのフィールドが異なっている

必要な場合にのみ新しいオブジェクト参照を返す

React Redux は浅い比較(===)を行って、mapStateToProps の結果が変更されたかどうかを確認する。
間違って毎回新しいオブジェクトまたは配列参照を返すことは簡単だが、これにより、データが実際に同じであってもコンポーネントが再レンダリングされてしまう。

下記のような一般的な操作で、新しいオブジェクトまたは配列参照が作成される。

  • Array.prototype.map(), Array.prototype.filter() を使用して新しい配列を作る
  • Array.prototype.concat() を使って配列をマージする
  • Array.prototype.slice() を使って配列の一部を抽出する
  • Object.assign を使って値をコピーする
  • スプレッド演算子 { ...oldState, ...newState }を使って値をコピーする

これらの操作をメモされた selector 関数に入れて、入力値が変更された場合のみに実行するようにする。
これにより、入力値が変更されていない場合、mapStateToProps は以前と同じ結果値を返し、connect は再レンダリングをスキップできる。

データが変更されたときにのみ高負荷な操作を実行する

多くの場合、データの変換には負荷がかかる(通常、新しいオブジェクト参照が作成される)。
mapStateToProps 関数を可能な限り高速にするには、関連データが変更されたときにのみこれらの複雑な変換を再実行するようにする。
これにアプローチする方法はいくつかある。

  • 一部の変換は Action Creator または reducer で計算でき、変換されたデータは store に保持できる
  • 変換は、コンポーネントrender() メソッドでも実行できる
  • mapStateToProps 関数で変換を行う必要がある場合は、メモ化された selector 関数を使用して、入力値が変更されたときにのみ変換が実行されるようにする
Immutable.jsのパフォーマンスの懸念

Immutable.js の著者である Twitter の Lee Byron は、パフォーマンスが懸念される場合は toJS を避ける​​ことを明示的に推奨している。

行動と落とし穴

store の state が同じ場合、mapStateToProps は実行されない

connect によって生成されたラッパーコンポーネントは、Redux store に subscribe する。
Action が dispatch されるたびに、 store.getState() を呼び出し、lastState === currentState かどうかを確認する。
2つの state の値が参照によって同一である場合、mapStateToProps 関数は再実行されない。これは、store state の残りの部分も変更されていないと想定しているため。

Redux の combineReducers は、このために最適化を試みる。
分割された reducer のいずれも新しい値を返さなかった場合、combineReducers は新しいオブジェクトではなく古い state オブジェクトを返す。
これは、reducer での変更によっては、root state オブジェクトが更新されない可能性があり、UIが再レンダリングされないことを意味する。

宣言された引数の数は動作に影響する

引数1つ (state) を使用すると、store の state オブジェクトが異なる場合、常にmapStateToProps 関数が実行される。
引数2つ (state、ownProps) を使用すると、store の state が異なるときはいつでも実行され、ラッパーコンポーネントのプロパティが変更されるたびに実行される。

つまり、実際に使用する必要がない限り、ownProps 引数を追加しないこと。
追加すると、mapStateToProps 関数が必要以上に実行される。

この動作の周辺にはいくつかのエッジケースがある。
必須引数の数は、mapStateToProps が ownProps を受け取るかどうかを決める。

  • 関数の引数が1つの場合、mapStateToProps は ownProps を受け取らない。
function mapStateToProps(state) {
  console.log(state) // state
  console.log(arguments[1]) // undefined
}
  • 関数の引数がゼロまたは2つの場合、ownProps を受け取る。
function mapStateToProps(state, ownProps) {
  console.log(state) // state
  console.log(ownProps) // ownProps
}

function mapStateToProps() {
  console.log(arguments[0]) // state
  console.log(arguments[1]) // ownProps
}

function mapStateToProps(...args) {
  console.log(args[0]) // state
  console.log(args[1]) // ownProps
}

所感

  • 一番気になったのは下記の selector 関数の説明。

    メモ化された selector 関数は、アプリケーションのパフォーマンスを向上させる上でも重要な役割を果たす。

この「メモ化された selector 関数」というのがピンと来ていないので、別途きちんと理解したいと思います。

  • mapStateToProps の使い方次第で connect されたコンポーネントが再レンダリングされるかしないかは大きいので、mapStateToPropsの実装がおかしくないか(不要な再レンダリングが行われていないか)Redux Devtool などを使って確認しながら実装すること