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

  • 124
    Like
  • 0
    Comment

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