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