react + reduxでComponentとContainerをどう使い分けるか?
デバッグ用のツールも導入した!
URLとComponentを一意に関連づけるためにredux-routerも導入した!
さあ、これからが本番です。
ページの構成を作り込んでいくターンだ!!
と意気込んだんですが、早速悩ましい問題が一つ。
Component ? or Container ?
react_on_railsで作成されたプロジェクトには、サンプルアプリがバンドルされるのですが、 そちらの構成が大体このような内容になっています。
HelloWorld アプリのファイル構成
client/app/bundle/HelloWorld/
actions/
helloWorldActionCreators.jsx
components/
HelloWorld.jsx
constants/
helloWorldConstants.jsx
containers/
HelloWorldContainer.jsx
reducers/
helloWorldReducer.jsx
startup/
HelloWorldApp.jsx
registration.jsx
store/
helloWorldStore.jsx
注目したいのは、HelloWorldContainer.jsx と HelloWorld.jsx の二つです。
(react+redux初心者としては、reducerってなんだろ?storeってなんだろ?storeだけなんで単数?とか色々思うところはあるんですが、ひとまずその二つにだけ注目)
client/app/bundles/HelloWorld/containers/HelloWorldContainer.jsx
// Simple example of a React "smart" component
import { connect } from 'react-redux';
import HelloWorld from '../components/HelloWorld';
import * as actions from '../actions/helloWorldActionCreators';
// Which part of the Redux global state does our component want to receive as props?
const mapStateToProps = (state) => ({ name: state.name });
// Don't forget to actually use connect!
// Note that we don't export HelloWorld, but the redux "connected" version of it.
// See https://github.com/reactjs/react-redux/blob/master/docs/api.md#examples
export default connect(mapStateToProps, actions)(HelloWorld);
まずは、Container側から確認します。
今の所「なんのこっちゃ」という感想くらいしか持てません。
connectってなんじゃろ?actionとは?mapStateToPropsとは?えーい、ひとまず先に進んだろ!
client/app/bundles/HelloWorld/components/HelloWorld.jsx
import React, { PropTypes } from 'react';
const HelloWorld = ({ name, updateName }) => (
<div>
<h3>
Hello, {name}!
</h3>
<hr />
<form >
<label htmlFor="name">
Say hello to:
</label>
<input
id="name"
type="text"
value={name}
onChange={(e) => updateName(e.target.value)}
/>
</form>
</div>
);
HelloWorld.propTypes = {
name: PropTypes.string.isRequired,
updateName: PropTypes.func.isRequired,
};
export default HelloWorld;
Component側はとても分かりやすくてビックリします。
このシンプルな二つの例を確認しただけで分かること(間違っていても、現段階で持てる認識として)は、、、
Containerとは?
reactとreduxをつなぐためのもの
Componentとは?
reduxが提供するstoreデータ、actionを受け取って動作する、最小構成の部品
(reduxの事を意識しなくてもいい、つまり、connectしない。connectが必要ならそれはContainerとして扱う)
という感じでしょうか?
さて、では、IndexPageというものを用意したい場合、HelloWorldアプリと同様に、
- IndexPage
- IndexPageContainer
を用意するべきでしょうか?
んー、直感的には「NO!」なんですよね。
react + redux初心者なんで、なんの確信もない、ただのイメージですが。
じゃあどうする?
そもそも何が気にくわないのか?
- IndexPage (Component)
- IndexPageContainer (Container)
この構成の何が僕はそんなに気にくわないのでしょうか?理由は色々あります。
冗長すぎないか?
- IndexPage内で利用するComponent群を、IndexPageContainerで意識しないといけない
- IndexPageContainer → IndexPage → IndexPage内の各種Componentまでのデータバケツリレーが発生しそう
IndexPageの再利用性の低さ
- 「Component」って、再利用性が高いイメージ
- IndexPageをComponentって呼びたくない
個人的にはやはり「PageはContainerとして扱いたい!」です。
最終的な構成イメージ
client/app/containers/IndexPage.jsx
import React, { Component } from 'react';
import { connect } from 'react-redux';
import InformationList from '../components/InformationList';
import * as actions from '../actions/indexPageActionCreators';
class IndexPage extends Component {
render() {
return (
<div>
Welcome to my site !
<InformationList informations={this.props.informations} />
</div>
);
}
}
const mapStateToProps = (state) => ({ informations: state.informations });
export default connect(mapStateToProps, actions)(IndexPage);
client/app/components/InformationList.jsx
import React, { Component, PropTypes } from 'react';
class InformationList extends Component {
static propTypes {
informations: PropTypes.array.isRequired,
}
render() {
return (
<ul className="informations">
{this.props.informations.map((information, index) => {
<Information {...information} />
})}
</ul>
);
}
}
class Information extends Component {
static propTypes {
title: PropTypes.string.isRequired,
body: PropTypes.string.isRequired,
}
render() {
return (
<li className="information">
<span class="title">{this.props.title}</span>
<span class="body">{this.props.body}</span>
</li>
);
}
}
export default InformationList;
うん、この構成でいけると色々いい感じかもしれない!
と、ここで一つ気づくことがあります。
Containerも結局のところComponentじゃん?
うん、どうもそうみたいですね。
connect対象がcomponentだし、componentはcomponentを内包できるし、つまるところ、Containerとは、
「connectされる事になる最上位のComponent」という認識でいいかなと思います。
間違っていたらごめんなさい。
執筆時のシステム構成
サーバーサイド
- ruby: 2.3.3p222
- rails: 5.0.2
- react_on_rails: 6.8.2
クライアントサイド
- react-on-rails: 6.8.2
- react-redux: ^5.0.3
- react-router: ^2.0.0
- redux: ^3.6.0
- redux-router: ^2.1.2
- history: ^2.0.0