Edited at

[redux] Presentational / Container componentの分離 - react-redux.connect()のつかいかた

ReduxをReactと同時につかうときは,コンポーネントをPresentational componentとContainer componentに分離することがプラクティスの一つらしいです.

このための道具としてconnect()関数がreact-reduxパッケージで提供されています.

http://redux.js.org/docs/basics/UsageWithReact.html

に詳しいです.

表示のみに専念するPresentational componentとロジックのみに専念するContainer componentを分離することで,コンポーネントの再利用性を高めることができる,らしいです.

自分の勉強のために自分なりの要点だけ以下にまとめてみました.

ES6記法を使っています.


Presentational component

Presentational componentsは見た目だけを扱うコンポーネントです.

基本的にstateには触らず,propsとして与えられるデータを表示することに専念します.

storeにもアクセスしません.なので,dispatchもできません.

例えばボタンを表示しても,onClickではpropsで与えられるコールバック関数を呼ぶだけです.

こうすることで,表示するデータや,ボタン押下時の処理を外部から指定することができ,再利用性が上がります.

追記 (編集リクエストありがとうございます): dropdownの開閉状態のような、componentの中に閉じ込めた方が良いと判断されるデータの管理にはstateを使うこともあります.そういうのは大抵UIに関する状態管理です.アプリケーションの状態やデータはReduxのstoreに格納し、container comoponentからアクセスすることになるでしょう.

たとえば,こんな感じ.

import React from 'react'

export default React.createClass({
render() {
const {
text,
onButtonClick,
} = this.props

return (
<div>
<p>{text}</p>
<button onClick={onButtonClick}>Click!</button>
</div>
)
}
})

表示するデータtextはpropsとして受け取り,表示します.具体的にtextが何になるかについて,Presentational componentは関知しません.

ボタン押下時のコールバックonButtonClickもpropsとして受け取ります.これも同様に,onButtonClickがどんな処理かについてはPresentational componentは関知しません.


Container component

Container componentはPresentational componentに具体的なデータやコールバック関数を与えるコンポーネントです.

react-reduxが提供するconnect()関数を使って,

import { connect } from 'react-redux'

import SomePresentationalComponent from 'some-presentational-compnent'

const mapStateToProps = (state, ownProps) => {
return {
// blah blah blah
}
}

const mapDispatchToProps = dispatch => {
return {
// blah blah blah
}
}

export default connect(
mapStateToProps,
mapDispatchToProps
)(SomePresentationalComponent)

のように書くもの,と思っておけばいいと思います.

mapStateToProps(state, ownProps)は,store.getState()の結果を第一引数に,このContainer componentへ渡されたpropsを第二引数にして呼び出される関数で,

これらのstateとpropsを使って子のPresentational componentにpropsとして渡す値を生成します.

mapDispatchToProps(dispatch)は,store.dispatchを第一引数にして呼び出される関数で,

子のPresentational componentにpropsとして渡す値(というか,コールバック関数)を生成します.通常,このコールバック関数では,引数として渡されているdispatchを呼び出し,子のPresentational ComponentがStoreへActionを送信できるようにしておきます.ボタン押下時に呼ばれる処理などもコールバック関数として書いてPresentational componentにpropsとして渡すと言いましたが,このコールバック関数を作るためのものと思っておけばいいと思います.

connect()を使った上記のコードは,

import React from 'react'

import SomePresentationalComponent from 'some-presentational-compnent'

export default React.createClass({
componentWillMount() {
this.unsubscribe = this.context.store.subscribe(this.forceUpdate)
},

componentWillUnmount() {
this.unsubscribe()
},

contextTypes: {
store: React.PropTypes.object,
},

render() {
const ownProps = this.props
const state = this.context.store.getState()
const dispatch = this.context.store.dispatch

const mapStateToProps = (state, ownProps) => {
return {
// blah blah blah
}
}

const mapDispatchToProps = (dispatch) => {
return {
// blah blah blah
}
}

const propsFromState = mapStateToProps(state, ownProps)
const propsFromDispatch = mapDispatchToProps(dispatch)

return <SomePresentationalComponent
{...propsFromState}
{...propsFromDispatch}
/>
}
})

と書くのと同じなのですが,


  • contextとして渡されるstoreを明示的に扱わなくて良い


  • store.subscribe(this.forceUpdate)周りの記述を省略できる(reduxのstateが変更されたら,勝手に表示を更新してくれる)


    • しかも自前でsubscribeするより最適化されている(らしい)



  • そもそもコード量が減る

などの利点があります.


connect()利用法

connect()を使うために,ルートノードをマウントするとき,

import ReactDOM from 'react-dom'

import { Provider } from 'react-redux'
import App from 'app.react'

const store = configureStore() // create store

ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)

のように一番外側を<Provider>で囲んで,storeを渡しておく必要があります.

また,Provider直下のコンポーネントはPresentational componentであるのがよいらしいです.

const App = React.createClass({

render() {
return (
<SomeContainerComponentA />
<SomeContainerComponentB />
<SomeContainerComponentC />
)
}
})

ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)

みたいな?

こうすると,ルートノードがContainer componentの羅列になってスッキリする?


ディレクトリ構成

あくまで例ですが,私は自分のプロジェクトのソースディレクトリをこんな感じにしています.

Presentational componentsをcomponents以下に,Container componentsをcontainers以下に格納して区別しています.

src

├── actions
├── app.js
├── components
│   ├── App.react.js
│   └── Page.react.js
├── containers
│   └── AwesomePage.react.js
└── store
├── configureStore.js
└── reducers
└── some-reducer.js