Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

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
tuttieee
最近はPythonでおしごと, TypeScript + Reactでindie hacking
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away