connect()

概要

connect()関数はReactコンポーネントをRedux storeに接続するために使います。

connect()関数(の戻り値)はstoreから必要なデータを抽出して接続済みコンポーネントに渡します。また、connect()関数(の戻り値)はactionをstoreにdispatchすることができる関数も接続済みコンポーネントに渡します。

connect()関数(の戻り値)は、渡されたコンポーネントクラスに変更を加えません。代わりに、渡されたコンポーネントクラスをラップしたstoreに接続した新しいコンポーネントクラスを返します。

function connect(mapStateToProps?, mapDispatchToProps?, mergeProps?, options?)

mapStateToPropsmapDispatchToPropsは、それぞれ、Redux storeのstatedispatchを扱います。statemapStateToProps関数の第1引数として提供されます。dispatchmapDispatchToProps関数の第1引数として提供されます。

mapStateToPropsmapDispatchToPropsの戻り値をそれぞれstatePropsdispatchPropsとします。それらはmergePropsが定義されていれば、第1引数と第2引数になります。ちなみに第3引数はownPropsです。それらを統合したmergePropsの結果はmergedPropsと言われます。それは接続済みコンポーネントに渡されます。

connect()の引数

connectは以下の4つの引数を受け取ります。それらはすべてオプションです。

  1. mapStateToProps?: Function
  2. mapDispatchToProps?: Function | Object
  3. mergeProps?: Function
  4. options?: Object

mapStateToProps?: (state, ownProps?) => Object

mapStateToProps関数がconnectに渡されると新しく生成されるラップしているコンポーネントはRedux storeの更新をsubscribeします。これはstoreが更新される度、mapStateToPropsが実行されることを意味します。mapStateToProps関数は素のobjectを返す必要があります。これはラップされたコンポーネントのpropsとマージされます。storeの更新をsubscribeしたくない場合は、mapStateToPropsの所にnullundefinedを渡します。

引数

  1. state: Object
  2. ownProps?: Object

mapStateToProps関数は最大2つの引数を受け取ります。関数の定義に設定された引数の数によって実行されるタイミングが異なります。つまり、定義時にownPropsを受け取るようにしているかしていないかです。 詳しくはこちらを見てください。

state

mapStateToProps関数定義の引数の数が1つの場合、mapStateToProps関数はstoreのstateが変更される時に実行されます。その時、store stateのみ引数として受け取ります。

const mapStateToProps = (state) => ({ todos: state.todos })
ownProps

mapStateToProps関数定義の引数の数が2つの場合、storeのstateが変更されるもしくは(浅い(shallow)比較に基づいた)新しいpropsを受け取った時に実行されます。それの第1引数はstoreのstateです。第2引数はラップしている(外側の)コンポーネントが受け取るpropsです。第2引数のことをownPropsと言います。

const mapStateToProps = (state, ownProps) => ({
  todo: state.todos[ownProps.id],
})

戻り値

mapStateToPropsの戻り値は、(Factory Functionの場合を除いて)objectである必要があります。そのobjectはstatePropsと言われます。statePropsは接続済みコンポーネントのpropsにマージ(merge)されます。statePropsmergePropsの第1引数として渡されます。

mapStateToPropsの戻り値によって接続済みコンポーネントが再レンダリングされるかが決まります。(詳しくはこちら)

mapStateToPropsを使いこなしたい場合は、こちらを見てください。

mapStateToPropsmapDispatchToPropsをfactory function(objectでなく関数を返す関数)として定義している場合、その戻り値の関数がmapStateToPropsmapDispatchToPropsとして扱われます。詳しくはFactory Functionsパフォーマンス最適化に関するガイドを見てください。

mapDispatchToProps?: Object | (dispatch, ownProps?) => Object

mapDispatchToPropsconnect()の第2引数です。これはobject、関数、渡されないかのいずれかです。

以下のようにconnect()の第2引数(mapDispatchToProps)を指定しない場合、コンポーネントはデフォルトでdispatchを受け取ります。

// `mapDispatchToProps`を渡さない。
connect()(MyComponent)
connect(mapStateToProps)(MyComponent)
connect(mapStateToProps, null, mergeProps, options)(MyComponent)

mapDispatchToPropsが関数の場合、それは最大で2つの引数を持つ関数として定義することができます。

引数

  1. dispatch: Function
  2. ownProps?: Object
dispatch

mapDispatchToPropsの引数が1つと定義されている場合、storedispatchが渡されます。

const mapDispatchToProps = (dispatch) => {
  return {
    // dispatching plain actions
    increment: () => dispatch({ type: 'INCREMENT' }),
    decrement: () => dispatch({ type: 'DECREMENT' }),
    reset: () => dispatch({ type: 'RESET' }),
  }
}
ownProps

mapDispatchToPropsの引数が1つと定義されている場合、第1引数にはdispatchが渡されます。第2引数にはラップしているコンポーネントに渡されたpropsが渡されます。新しいpropsが接続済みコンポーネントに渡される度、mapDispatchToProps関数が実行されます。

第2引数はownPropsと言われます。

// binds on component re-rendering
<button onClick={() => this.props.toggleTodo(this.props.todoId)} />

// binds on `props` change
const mapDispatchToProps = (dispatch, ownProps) => ({
  toggleTodo: () => dispatch(toggleTodo(ownProps.todoId)),
})

mapDispatchToPropsで定義されている引数の数によってownPropsが渡されるか決まります。詳しくはこれを見てください。

戻り値

mapDispatchToProps関数はobjectを返す必要があります。そのobjectの各フィールドは関数でなければなりません。その関数が実行されるとactionがstoreにdispatchされます。mapDispatchToProps関数の戻り値はdispatchPropsと言われます。接続済みコンポーネントのpropsにマージされます。dispatchPropsmergePropsの第2引数になります。

const createMyAction = () => ({ type: 'MY_ACTION' })
const mapDispatchToProps = (dispatch, ownProps) => {
  const boundActions = bindActionCreators({ createMyAction }, dispatch)
  return {
    dispatchPlainObject: () => dispatch({ type: 'MY_ACTION' }),
    dispatchActionCreatedByActionCreator: () => dispatch(createMyAction()),
    ...boundActions,
    // こうやってdispatchを渡すこともできます。
    dispatch,
  }
}

mapDispatchToPropsを使いこなしたい場合は、こちらを見てください。

mapStateToPropsmapDispatchToPropsをfactory function(objectでなく関数を返す関数)として定義している場合、その戻り値の関数がmapStateToPropsmapDispatchToPropsとして扱われます。詳しくはFactory Functionsパフォーマンス最適化に関するガイドを見てください。

Object Shorthand Form

以下のように、mapDispatchToPropsaction creatorを値に持つobjectにすることもできます。

import { addTodo, deleteTodo, toggleTodo } from './actionCreators'

const mapDispatchToProps = {
  addTodo,
  deleteTodo,
  toggleTodo,
}

export default connect(null, mapDispatchToProps)(TodoApp)

このケースではReact-ReduxはbindActionCreatorsを使って各action creatorをstoreのdispatchにbindします。mapDispatchToPropsの戻り値はdispatchPropsと言われます。接続済みコンポーネントのpropsにマージされるかmergePropsの第2引数になります。

// internally, React-Redux calls bindActionCreators
// to bind the action creators to the dispatch of your store
bindActionCreators(mapDispatchToProps, dispatch)

mapDispatchToPropsのObject Shorthand Formを使ったやり方について詳しく知りたい場合はこちらを見てください。

mergeProps?: (stateProps, dispatchProps, ownProps) => Object

mergePropsはラップされたコンポーネントに最終的に渡るpropsを決定します。mergePropsがconnect関数に渡されてない場合、ラップされたコンポーネントにはデフォルトで{ ...ownProps, ...stateProps, ...dispatchProps } が渡されます。

引数

mergePropsは最大で3つの引数を渡すことができます。それらの引数は、それぞれ、mapStateToProps()mapDispatchToProps()の戻り値とラップしているコンポーネントのpropsです。

  1. stateProps
  2. dispatchProps
  3. ownProps

mergePropsが返す素のobjectにある各値はラップされたコンポーネントのpropsになります。この関数でpropsに応じてstateの一部らから更に値を抽出したり、action creatorに特定のpropsの値をbindしたりすることができます。

戻り値

mergePropsの戻り値はmergedPropsと言われます。これの各値はラップされたコンポーネントのpropsになります。

options?: Object

{
  context?: Object,
  pure?: boolean,
  areStatesEqual?: Function,
  areOwnPropsEqual?: Function,
  areStatePropsEqual?: Function,
  areMergedPropsEqual?: Function,
  forwardRef?: boolean,
}

context: Object

注意: この変数は6.0以上でサポートされています。

React-Redux 6.0からReact-Redux内で使用できるカスタムcontextインスタンスを設定できるようになりました。contextインスタンスを<Provider />と接続済みコンポーネントに渡す必要があります。contextインスタンスを接続済みコンポーネントに渡す方法は、このオプションを使う方法とレンダリング時に接続済みコンポーネントのprops経由で渡す方法があります。

// const MyContext = React.createContext();
connect(mapStateToProps, mapDispatchToProps, null, { context: MyContext })(
  MyComponent
)

pure: boolean

ラップされたコンポーネントはpure component(propsのみに依存するコンポーネントのことでPureComponentではない。)とみなされ、propsとRedux Storeのstate以外の入力やstateには依存しないとみなされます。

options.pureがtrueの場合、connectは不要なmapStateToPropsmapDispatchToPropsmergePropsrenderの呼び出しを予防するために複数の等価チェックを行います。その等価チェックはareStatesEqualareOwnPropsEqualareStatePropsEqualareMergedPropsEqualです。これらの関数はデフォルトの物で99%の場合で問題ありませんが、パフォーマンスや別の理由でこれらを置き換えたいかもしれません。

以下でそれらの例を示します。

areStatesEqual: (next: Object, prev: Object) => boolean

pureが有効な場合、現在のstore stateと1つ前のそれを比較します。

例 1

const areStatesEqual = (next, prev) =>
  prev.entities.todos === next.entities.todos

mapStateToPropsの処理コストが高い、かつ、それがstateのごく一部のみに依存している場合はareStatesEqualを上書きすると良いと思います。上の例では指定したstateの一部以外のstateの変化は無視されます。

例 2

store stateを変更するimpure reducersが存在している場合、areStatesEqual は常にfalseを返すと良いと思います。

const areStatesEqual = () => false

おそらく、これはmapStateToProps関数によって他の等価チェックに影響を及ぼすと思います。

areOwnPropsEqual: (next: Object, prev: Object) => boolean

pureが有効な場合、現在のpropsと1つ前のpropsを比較します。

現在のpropsを通過させたい場合、areOwnPropsEqualを上書きすると良いと思います。propsを通過させるには、mapStateToPropsmapDispatchToPropsmergePropsを実装する必要があります。(recomposeのmapPropsのような別の方法の方が簡単かもしれません。)

areStatePropsEqual: (next: Object, prev: Object) => boolean

pureが有効な場合、現在のmapStateToProps戻り値と1つ前のそれを比較します。

areMergedPropsEqual: (next: Object, prev: Object) => boolean

pureが有効な場合、現在のmergeProps戻り値と1つ前のそれを比較します。

mapStateToPropsが関連するpropsが変更された場合のみ新しいobjectを返すメモ化されたselectorを使っているなら、areStatePropsEqualを上書きしてstrictEqualにした方が良いと思います。mapStateToPropsが実行される度、各propsの余計な等価性チェックを避けることができるので少しだけパフォーマンスが改善します。

selectorが複雑なpropsを生成する場合、areMergedPropsEqualを上書きしてdeepEqualな実装にすると良いと思います。(たぶん、deep equalは再レンダリングよりも速いと思います。)

forwardRef: boolean

注意: このパラメータはバージョン6.0からサポートされています。

{forwardRef : true}connectに渡されると、refをラップしている接続済みコンポーネントに渡すとref.currentは内側のラップされたコンポーネントを参照します。

connect()の戻り値

connect()はコンポーネントを受け取って、注入される追加のpropsを持つそれをラップしているコンポーネントを返すラッパー関数です。

import { login, logout } from './actionCreators'

const mapState = (state) => state.user
const mapDispatch = { login, logout }

// 1回目: コンポーネントをラップするために使うHOCを返します。
const connectUser = connect(mapState, mapDispatch)

// 2回目: mergedPropsを持つラップしているコンポーネントを返します。
// HOCを使うと異なるコンポーネントに同じ動作を追加することができます。
const ConnectedUserLogin = connectUser(Login)
const ConnectedUserProfile = connectUser(Profile)

通常、ラッパー関数は変数に格納されることなく、すぐに実行されます。

import { login, logout } from './actionCreators'

const mapState = (state) => state.user
const mapDispatch = { login, logout }

// connect関数を実行して、すぐにラッパー関数を実行して最終的なラッパーコンポーネントを生成します。

export default connect(mapState, mapDispatch)(Login)

使用例

connectはとても柔軟なので、いろいろな使い方ができます。その使用例を見ていきましょう。

export default connect()(TodoApp)
import * as actionCreators from './actionCreators'

export default connect(null, actionCreators)(TodoApp)

以下のようにしないでください。TodoAppはstateが変更される度、再レンダリングされるのでパフォーマンスの最適化が犠牲になります。 だから、個々のコンポーネントが必要なstaetの部分のみをsubscribeするようにした方が良いです。

// これはダメ!
export default connect((state) => state)(TodoApp)
function mapStateToProps(state) {
  return { todos: state.todos }
}

export default connect(mapStateToProps)(TodoApp)
import * as actionCreators from './actionCreators'

function mapStateToProps(state) {
  return { todos: state.todos }
}

export default connect(mapStateToProps, actionCreators)(TodoApp)

- todosとすべてのaction creator(addTodo, completeTodo, …)をactionsとして注入します。

import * as actionCreators from './actionCreators'
import { bindActionCreators } from 'redux'

function mapStateToProps(state) {
  return { todos: state.todos }
}

function mapDispatchToProps(dispatch) {
  return { actions: bindActionCreators(actionCreators, dispatch) }
}

export default connect(mapStateToProps, mapDispatchToProps)(TodoApp)
import { addTodo } from './actionCreators'
import { bindActionCreators } from 'redux'

function mapStateToProps(state) {
  return { todos: state.todos }
}

function mapDispatchToProps(dispatch) {
  return bindActionCreators({ addTodo }, dispatch)
}

export default connect(mapStateToProps, mapDispatchToProps)(TodoApp)
import { addTodo, deleteTodo } from './actionCreators'

function mapStateToProps(state) {
  return { todos: state.todos }
}

const mapDispatchToProps = {
  addTodo,
  deleteTodo,
}

export default connect(mapStateToProps, mapDispatchToProps)(TodoApp)
import * as todoActionCreators from './todoActionCreators'
import * as counterActionCreators from './counterActionCreators'
import { bindActionCreators } from 'redux'

function mapStateToProps(state) {
  return { todos: state.todos }
}

function mapDispatchToProps(dispatch) {
  return {
    todoActions: bindActionCreators(todoActionCreators, dispatch),
    counterActions: bindActionCreators(counterActionCreators, dispatch),
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(TodoApp)
import * as todoActionCreators from './todoActionCreators'
import * as counterActionCreators from './counterActionCreators'
import { bindActionCreators } from 'redux'

function mapStateToProps(state) {
  return { todos: state.todos }
}

function mapDispatchToProps(dispatch) {
  return {
    actions: bindActionCreators(
      { ...todoActionCreators, ...counterActionCreators },
      dispatch
    ),
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(TodoApp)
import * as todoActionCreators from './todoActionCreators'
import * as counterActionCreators from './counterActionCreators'
import { bindActionCreators } from 'redux'

function mapStateToProps(state) {
  return { todos: state.todos }
}

function mapDispatchToProps(dispatch) {
  return bindActionCreators(
    { ...todoActionCreators, ...counterActionCreators },
    dispatch
  )
}

export default connect(mapStateToProps, mapDispatchToProps)(TodoApp)
import * as actionCreators from './actionCreators'

function mapStateToProps(state, ownProps) {
  return { todos: state.todos[ownProps.userId] }
}

export default connect(mapStateToProps)(TodoApp)
import * as actionCreators from './actionCreators'

function mapStateToProps(state) {
  return { todos: state.todos }
}

function mergeProps(stateProps, dispatchProps, ownProps) {
  return Object.assign({}, ownProps, {
    todos: stateProps.todos[ownProps.userId],
    addTodo: (text) => dispatchProps.addTodo(ownProps.userId, text),
  })
}

export default connect(mapStateToProps, actionCreators, mergeProps)(TodoApp)

注意点

mapToProps関数の引数

mapStateToPropsmapDispatchToProps関数定義の引数の数によって、引数にownPropsを受け取るか決まります。

mapStateToPropsmapDispatchToProps関数定義関数定義で引数の数が1つ(func.lengthが1)の場合、ownPropsmapStateToPropsmapDispatchToProps関数に渡されません。例えば、以下のように定義された関数は第2引数にownPropsを受け取りません。ownPropsundefinedの場合、デフォルトの値が使われます。

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

const mapStateToProps = (state, ownProps = {}) => {
  console.log(state) // state
  console.log(ownProps) // {}
}
// console.log(mapStateToProps.length) => 1

関数が0個または2以上の個数の引数を受け取るように定義されている場合、ownPropsが渡されます。

const mapStateToProps = (state, ownProps) => {
  console.log(state) // state
  console.log(ownProps) // ownProps
}

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

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

Factory Functions

mapStateToPropsもしくはmapDispatchToProps関数が関数を返す場合、コンポーネントがインスタンス化する際に一度だけ実行されます。それらの戻り値は、それ以後、それぞれmapStateToPropsmapDispatchToProps関数として取り扱われます。

factory functionsはメモ化されたselectorによく使われます。これによって、クロージャー内にコンポーネントインスタンスごとにselectorを作成することが可能になります。

const makeUniqueSelectorInstance = () =>
  createSelector([selectItems, selectItemId], (items, itemId) => items[itemId])
const makeMapState = (state) => {
  const selectItemForThisComponent = makeUniqueSelectorInstance()
  return function realMapState(state, ownProps) {
    const item = selectItemForThisComponent(state, ownProps.itemId)
    return { item }
  }
}
export default connect(makeMapState)(SomeComponent)

License

Japanese part

Attribution-NonCommercial-NoDerivatives 4.0 International (CC BY-NC-ND 4.0)

Copyright (c) 2021 38elements

Other

The MIT License (MIT)

Copyright (c) 2015-present Dan Abramov

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.