大事なのはmergeProps、そして退かぬ心じゃ

エンジニアの原です。

最近なぜかreact-reduxの解説記事をやたら目にしますが何かあったのでしょうか?

気になるのが解説記事の中であまりmergePropsの重要性が語られていないことです。

https://redux.js.org/basics/example-todo-list

すべての元凶はこのサンプルでしょうか…?

今回はmapStateToPropsとmergePropsの役割とその重要性についてちょっと書いてみたいと思います。

reduxのサンプルに問題があるとして下記のQiitaの記事で指摘がされています。

https://qiita.com/zaki-yama/items/5258e6f1ae37f63034b9

このQiitaの記事で解決したい課題として

getVisibleTodos() は 関係する state.todos もしくは state.visibilityFilterに更新があったかどうかに関わらず state が更新されるたびに実行されるので、フィルタリング処理の計算コストが高かった場合パフォーマンスに影響が出てしまう。

とあります。

そうですね。

しかしこれはそもそもmapStateToPropsで計算をしている事自体が間違いです(公式サンプル全否定)。

と言い切るのもどうかと思いますが、パフォーマンスを考えたときにmapStateToPropsとmergePropsの処理を考えるとmapStateToPropsで計算処理をすることは間違いと言っても過言では無いと思います。

そもそもmapStateToPropsとmergePropsという名前が全然役割を表して無くてこれが良くないと思ってます。

react-reduxの仕事

react-reduxのmapStateToPropsはこの名前から、「ReduxStateから取り出した値をComponentに渡したい形に変更する」と思われがち(っていうかreduxのサンプルがあれですからね)ですが、

本来の仕事は変更を監視したいReduxStateの選別です。

react-reduxの仕事として

があります。

react-reduxはReduxStoreの値を監視していていどれかに変更があると処理を行います。

なので、「ReduxStateに変更があったとき」が始まりで「コンポーネントレンダリングするか否か(renderを実行するか)」が結果になります。

処理は以下の流れです(わかりやすくするためにOwnPropsは変更されない, connectのoptionは未定義(デフォルト)と仮定します)。

  1. ReudxStateの参照が同じか(===比較)?
  2. mapStateToPropsを計算。
  3. 前回のmapStateToPropsと結果同じか(shallow比較)?
  4. megePropsを計算
  5. 前回のmergePropsと結果が同じか(mergeProps未定義? ===比較 : shallow比較)?

mergePropsは定義してなくても、mapStateToPropsを定義しているとデフォルトの

{...mapStateToPropsResult, ...mapDispatchToPropsResulg, ...ownProps}

が実行されます。

mergePropsは未定義の場合、前回の結果と===で比較が行われるため必ずfalseになります。

mergePropsではmapStateToPropsを元に計算したりだとか、Componentに渡したくない(表示に関係ない)けどクリックイベントなどによってAPIを叩いたりするときに必要なActionを定義したりすると良いです。

たとえばReduxStateの値からあらたに値を生成する時に、Stateの値は変わってるけど計算結果が変わらない場合、mergePropsで適切に処理をすると無駄なレンダリングをしなくて済みます。

const mapStatToProps = (state) => ({
  scoreA: state.scoreA,
  scoreB: state.scoreB,
});

const mergeProps = (mapStatToProps) => ({
  score: mapStatToProps.scoreA + mapStateToProps.scoreB
})


/* もともと scoreA = 10, scoreB = 2でscoreA = 8, scoreB = 4になったとしても、
前回のmergePropsの比較で同じと判定されるのでレンダリングが行われない
/*

では、Qiitaで述べられている解決したい課題をreselectを使わずに解決します。

import { connect } from 'react-redux'
import { toggleTodo } from '../actions'
import TodoList from '../components/TodoList'

const getVisibleTodos = (todos, filter) => {
  switch (filter) {
    case 'SHOW_COMPLETED':
      return todos.filter(t => t.completed)
    case 'SHOW_ACTIVE':
      return todos.filter(t => !t.completed)
    case 'SHOW_ALL':
    default:
      return todos
  }
}

const mapStateToProps = ({ todos, visibilityFilter }) => ({
  todos, 
  visibilityFilter
})

const mapDispatchToProps = dispatch => ({
  toggleTodo: id => dispatch(toggleTodo(id))
})

const mergeProps = ({ todos, visibilityFilter}, mapDispatchToProps) => ({
  todos: getVisibleTodos(todos, visibilityFilter)
})

export default connect(
  mapStateToProps,
  mapDispatchToProps,
  mergeProps
)(TodoList)

解決したい問題の

state.todos もしくは state.visibilityFilter に更新があったかどうかに関わらず state が更新されるたびに実行されるの

ですが、上記の処理に当てはめると

仮にtodos, visibilityFilterに関係ないReduxStateが変更された場合

  1. ReudxStateの参照が同じか(===比較)?

falseなので2へ

  1. mapStateToPropsを計算。

todos, visibilityFilterを参照するのみです。

  1. 前回のmapStateToPropsと結果が同じか(shallow比較)?

trueでレンダリングも行われず処理は終了します。

となり、無駄にgetVisibleTodosが実行されるのを防げます。

入門系の記事だとmeregePropsの内容が疎かだったりするのですが、パフォーマンスを考える際にはmergePropsの理解は必須です。

react-reudxはコード量もそんなに多くないので読んでみる事をお薦めします。