323
245

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2016-03-01

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
323
245
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
323
245

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?