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

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

デバッグ用のツールも導入した!
URLとComponentを一意に関連づけるためにredux-routerも導入した!

さあ、これからが本番です。
ページの構成を作り込んでいくターンだ!!

と意気込んだんですが、早速悩ましい問題が一つ。

Component ? or Container ?

react_on_railsで作成されたプロジェクトには、サンプルアプリがバンドルされるのですが、 そちらの構成が大体このような内容になっています。

HelloWorld アプリのファイル構成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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

1
2
3
4
5
6
7
8
9
10
11
12
13
// 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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