react + reduxでComponentとContainerをどう使い分けるか?

木内智史之介(シャッチョー)
ミンカさんけっこんしてくださいおねがいします(ズザー SEGAさん、DIVAの筐体ください(ズザー

デバッグ用のツールも導入した!
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.jsxHelloWorld.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