Container / Presentational Component

Container Component

ビューを作らないコンポーネント = データをstateとして扱い供給するためのコンポーネント

Presentational Component

Containerからpropsを受取り、ビューを描画するコンポーネント

Ajaxでデータを取得する例

Container ComponentとPresentational Componentの組み合わせに対して、よろしくない例として以下のようなコンポーネントを設計してしまうことです。

class UserList extends Component {  
  constructor(props) {
    super(props);

    this.state = {
      users: []
    };

    this.fetchUsers = this.fetchUsers.bind(this);
  }

  componentDidMount() {
    this.fetchUsers();
  }

  fetchUsers() {
    fetch('/users.json')
      .then((res) => {
        return res.json();
      })
      .then((json) => {
        this.setState({ users: json });
      });
  }

  render() {
    return (
      <section className="users">
        <h2>UserList</h2>
        <ul>
        {this.state.users.map((user) => {
          return (
            <li key={user.id}>{user.name} : {user.age}</li>
          );
        })}
        </ul>
      </section>
    );
  }
}

このコンポーネントでは「振る舞い」と「ビューを描画」するという分離するべきものが一緒になってます。これをContainerとPresentatinalなコンポーネントに分離していきます。

Presentational Component

Presentational Componentは自分のpropsがどこから来たものかに関与せず、stateを持たないコンポーネントです。上記のUserListコンポーネントをPresentationalなコンポーネントにしてみます。

Ajaxの部分を無くし、propsで受け取ったリストを表示するだけのコンポーネントになります。

class UserList extends Component {  
  render() {
    return (
      <section className="users">
        <h2>UserList</h2>
        <ul>
        {this.props.users.map((user) => {
          return (
            <li key={user.id}>{user.name} : {user.age}</li>
          );
        })}
        </ul>
      </section>
    );
  }
}

Container Component

Container ComponentはPresentational Componentの親として、データを供給してあげます。

class UserListContainer extends Component {  
  constructor(props) {
    super(props);

    this.state = {
      users: []
    };

    this.fetchUsers = this.fetchUsers.bind(this);
  }

  componentDidMount() {
    this.fetchUsers();
  }

  fetchUsers() {
    fetch('/users.json')
      .then((res) => {
        return res.json();
      })
      .then((json) => {
        this.setState({ users: json });
      });
  }

  render() {
    return (
      <UserList users={this.state.users} />
    );
  }
}

Container Componentは、普通のReactコンポーネントと同じです。ただし、自分自身で何かを描画することはなく、常に子のPresentational Componentの結果を描画するだけにしておきます。

Eventのハンドリング

Presentational Componentが何らかの振る舞いを持つ場合はどうするか。以下のような場合を考えてみます。

class UserList extends Component {  
  toggleSelect() {
    console.log('do something');
  }

  render() {
    return (
      <section className="users">
        <h2>UserList</h2>
        <ul>
        {this.props.users.map((user) => {
          return (
            <li key={user.id}>
              <span>{user.name}</span>
              <span>{user.age}</span>
              <button onClick={this.toggleSelect}>Select</button>
            </li>
          );
        })}
        </ul>
      </section>
    );
  }
}

問題になるのは、ボタンがクリックされたら何かしらのデータを変更したい場合です。その場合はstateを持つことになるはずです。この場合は、Container Componentから振る舞いも渡してもらうようにします。

class UserList extends Component {  
  render() {
    return (
      <section className="users">
        <h2>UserList</h2>
        <ul>
        {this.props.users.map((user) => {
          return (
            <li key={user.id}>
              <span>{user.name}</span>
              <span>{user.age}</span>
              {user.selected ? <span>Selected</span> : <span></span>}
              <button onClick={this.props.toggleSelect.bind(null, user.id)}>
                Select
              </button>
            </li>
          );
        })}
        </ul>
      </section>
    );
  }
}
class UserListContainer extends Component {  
  constructor(props) {
    super(props);

    this.state = {
      users: []
    };

    this.fetchUsers = this.fetchUsers.bind(this);
    this.toggleSelect = this.toggleSelect.bind(this);
  }

  componentDidMount() {
    this.fetchUsers();
  }

  fetchUsers() {
    fetch('/users.json')
      .then((res) => {
        return res.json();
      })
      .then((json) => {
        this.setState({ users: json });
      });
  }

  toggleSelect(userId) {
    let newState = Object.assign({}, this.state);

    let user = newState.users.find((user) => {
      return user.id === userId;
    });

    user.selected = !user.selected;
    this.setState(newState);
  }

  render() {
    return (
      <UserList users={this.state.users} toggleSelect={this.toggleSelect}/>
    );
  }
}

Stateless Component

UserListのようなstateを持たない、他のコンポーネントの1要素として機能するコンポーネントはStateless functional componentsとして、単に関数として書くことができます。(React > 0.14)

export default function UserList(props) {  
  return (
    <section className="users">
      <h2>UserList</h2>
      <ul>
      {props.users.map((user) => {
        return (
          <li key={user.id}>
            <span>{user.name}</span>
            <span>{user.age}</span>
            {user.selected ? <span>Selected</span> : <span></span>}
            <button onClick={props.toggleSelect.bind(null, user.id)}>
              Select
            </button>
          </li>
        );
      })}
      </ul>
    </section>
  );
}

より、シンプルにコンポーネントを書くことができますが、コンポーネントがrenderメソッドだけを持つに使えます。