Takahiro Octopress Blog

-1から始める情弱プログラミング

Connectについて学ぼう!〜iOSエンジニアが苦しんだReduxの基礎(3)〜

はじめに

前回のiOSエンジニアが苦しんだReduxの基礎(2)で素のReactと素のReduxを組み合わせたExampleについて見ていきました。
今回はconnect()を使ったReact&ReduxによるWebサイトの実装について見ていきたいと思います。
では早速見ていきましょう。

Counterサンプルで学ぼう!

公式ReduxページのExampleの先頭に書かれている Counter を見ていきましょう。
本来は素のReactと素のReduxを使ったExampleではあるのですが、今回の説明のために改変します。
実装するWebサイトは下図の通りです。

Counter Example画面

実装されている機能としては下記の4つになります。

  • 「+」ボタンを選択するとClick数が+1される
  • 「-」ボタンを選択するとClick数が-1される
  • 「Increment if odd」ボタンを選択するとClick数が奇数のときのみ+1される
  • 「Increment async」ボタンを選択すると1秒後にClick数が+1される

前回と異なるのはReactとReduxの連携に connect() を利用しているという点です。
では1つ1つ見ていきましょう。

フォルダ構成

まずはフォルダ構成を見ていきます。
(説明のために一部変更を加えています。)

1
2
3
4
5
6
7
8
9
10
11
12
counter
├── public
     └── index.html
└── src
     ├── index.js
     ├── actions
          └── index.js
     ├── reducers
          └── index.js
     ├── components
          └── Counter.js
     └── node_modules

素の連携とconnect()による連携の比較

Actions, Reducers, Componentsに関しては、前回と同じなので割愛します。
前回は、ReactとReduxの連携部分を下記のように書いていました。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// src/index.js
import React from 'react'
import ReactDOM from 'react-dom'
import { createStore } from 'redux'
import Counter from './components/Counter'
import counter from './reducers'
import { increment, decrement } from './actions'

const store = createStore(counter)
const rootEl = document.getElementById('root')

const render = () => ReactDOM.render(
  <Counter
    value={store.getState()}
    onIncrement={() => store.dispatch(increment())}
    onDecrement={() => store.dispatch(decrement())}
  />,
  rootEl
)

render()
store.subscribe(render)

今回はこの連携をconnect()を利用して実装します。

まずは、必要なモジュールを追加しましょう。

1
2
3
4
5
6
7
8
9
// src/index.js
import React from 'react'
import ReactDOM from 'react-dom'
import { createStore } from 'redux'
// Providerとconnectを追加
import { Provider, connect } from 'react-redux'
import Counter from './components/Counter'
import { increment, decrement } from './actions/index'
import counter from './reducers'

connect()で連携します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// src/index.js
function mapStateToProps (state) {
  return { value: state }
}

function mapDispatchToProps (dispatch) {
  return {
    onIncrement: () => dispatch(increment()),
    onDecrement: () => dispatch(decrement())
  }
}

let AppContainer = connect(
  mapStateToProps,
  mapDispatchToProps
)(Counter)

const render = () => ReactDOM.render(
  <Provider store={store}>
    <AppContainer />
  </Provider>,
  rootEl
)
render()

1つ1つ見ていきましょう。
connect()は4つの引数をセットできるのですが、中でも重要なのが次の2つです。

  1. mapStateToProps
  2. mapDispatchToProps

です。
全てはreact-reduxのReadmeに書かれているのですが、ソースと合わせて見ていきます。
まずは、mapStateToPropsからです。
Readmeには、

If specified, the component will subscribe to Redux store updates. Any time it updates, mapStateToProps will be called. Its result must be a plain object*, and it will be merged into the component’s props. If you omit it, the component will not be subscribed to the Redux store.

と書かれていますね。
これはソースで比較しても明らかです。
前回までは、『 Stateの変更結果として描画に反映させる 』ために下記のようにしていました。

1
2
// subscribeの第一引数にrenderメソッドを指定
store.subscribe(render)

上記のように、store.subscriberenderメソッドを渡すことで、dispatch実行してStateの状態が変化したときに、毎回renderメソッドが実行されていました。
今回はconnect()を利用しているのでstore.subscribeが書かれていないことがわかると思います。

1
2
3
4
5
6
7
8
9
10
11
// src/index.js
function mapStateToProps (state) {
  return { value: state }
}

// connectメソッドの第一引数は「stateを引数に持つメソッド」
// connectメソッドの第一引数にmapStateToPropsを設定
let AppContainer = connect(
  mapStateToProps,
  mapDispatchToProps
)(Counter)

ですが、やっていることは同じで『Stateが変更されてStoreにそれが伝えられたときに mapStateToPropsは毎回実行 されます。』 もし、mapStateToPropsconnect()の第一引数に指定しなかった場合、『 Stateの変更結果として描画に反映させる 』ことができません。

また、return { value: state }をすることでCounter ComponentpropTypesであるvalueに値を渡しています。

続いて、mapDispatchToPropsを見ていきます。
Readmeには、

If an object is passed, each function inside it will be assumed to be a Redux action creator. An object with the same function names, but with every action creator wrapped into a dispatch call so they may be invoked directly, will be merged into the component’s props. If a function is passed, it will be given dispatch.

と書かれていますね。
これは少々わかりづらいのですが、Counter Componentがクリックしたタイミングでstore.dispatchAction Creatorsであるincrementdecrementで作成したActionsを渡せるように実装することを実現しています。
前回までは、『 StoreにStateの変更を知らせる 』ために下記のようにしていました。

1
2
3
4
5
6
7
8
9
// src/index.js
const render = () => ReactDOM.render(
  <Counter
    value={store.getState()}
    onIncrement={() => store.dispatch(increment())}
    onDecrement={() => store.dispatch(decrement())}
  />,
  rootEl
)

このように直接Counter ComponentonIncrementおよびonDecrementを渡していました。
connect()を利用すると、第二引数のmapDispatchToPropsの戻り値としてAction Creatorsを設定することで実現できます。
これにより、Counter ComponentpropTypesであるonIncrementonDecrementに値を渡すことができます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/index.js
function mapDispatchToProps (dispatch) {
  return {
    onIncrement: () => dispatch(increment()),
    onDecrement: () => dispatch(decrement())
  }
}

// connectメソッドの第二引数は「dispatchを引数に持つメソッド」
// connectメソッドの第二引数にmapDispatchToPropsを設定
let AppContainer = connect(
  mapStateToProps,
  mapDispatchToProps
)(Counter)

まとめ

これでconnect()が何をしているのかが少しは見えてきました。
実行している処理内容がわかってくると、

  • connect(): ReactとReduxをconnect(接続)する
  • mapStateToProps: StatePropsmap(マッピング)する
  • mapDispatchToProps: DispatchPropsmap(マッピング)する

のように名称がそのものを表していたことが改めてわかります。
(理解促進してくれるような名称になっていますね。)

次回はさらに処理を簡略化して書くのに使われるredux-actionsを利用した実装について見ていきたいと思います。

Comments