LoginSignup
3

More than 5 years have passed since last update.

Reactコンポーネントで責務を分担する

Last updated at Posted at 2016-04-11

Reactコンポーネントで責務を分担するには(あんまり難しいことはなしで…)。

複雑なview等で、様々な状態をstateに保持したいとき、コンポーネントが肥大化してしまうのをなんとかしたい。(一応、storeには関係ない要素という想定)


'use strict'

import React, { Component } from 'react'

class SmartComponent extends Component {
  constructor(props) {
    super(props)
    this.state = {
      // stateたくさん
      hoge: 0,
      fuga: 1,
      piyo: 2,
      ...
    }
  }
  // state操作たくさん
  operateHoge() {...}
  operateHoge2() {...}
  operateFuga() {...}
  operateFuga2() {...}
  operatePiyo() {...}
  ...
  // イベントハンドリングたくさん
  handleClick() {...}
  handleMouseEnter() {...}
  handleMouseLeave() {...}
  handleClickHoge() {...}
  handleClickFuga() {...}
  ...
  // Component Lifecycleも入る・・・
  componentDidMount() {...}
  componentWillReceiveProps() {...}
  shouldComponentUpdate() {...}
  ...
  render() {...}
}

プロトタイプを拡張する方法

従来のようにprotptypeを拡張することもできるみたい。単に関数を分離したいとかならこれもありかも。

でも、今回はローカルstateも含めて責務を分離したいんですよね。aの関数と状態はAComponentに、bの関数と状態はBComponentに、みたいな感じで。

Mixinは死んだ

ES6だとモジュール入れないとmixinが使えません。不用意に依存モジュールを増やしたくない所です。また、今後mixinがサポートされることもなさそうです。

Higher-Order Componentsという方法が紹介されています。

こちらも参考になります。

コンポーネントの拡張

HOCを参考にしてコンポーネントを拡張するコードを書いてみました。


'use strict'

import React, { Component } from 'react'
import PresentationComponent from './presentation-component'

// hogeについての責務
function connectHogeDecorator(InnerComponent) {
  class HogeDecorator extends Component {
    constructor(props) {
      super(props)
      this.state = { hoge: 0 }
    }
    hogeOperator() {
      // operate hoge
    }
    handleClickHoge(e) {
      this.hogeOperator()
    }
    render() {
      return (
        <InnerComponent {...this.props} {...this.state}
          onClickHoge={this.handleClickHoge.bind(this)}
        />
      )
    }
  }
  return HogeDecorator
}

// fugaについての責務
function connectFugaDecorator(InnerComponent) {
  class FugaDecorator extends Component {
    constructor(props) {
      super(props)
      this.state = { fuga: 1 }
    }
    fugaOperator() {
      // operate fuga
    }
    handleClickFuga(e) {
      this.fugaOperator()
    }
    render() {
      return (
        <InnerComponent {...this.props} {...this.state}
          onClickFuga={this.handleClickFuga.bind(this)}
        />
      )
    }
  }
  return FugaDecorator
}

// ベースコンポーネント
export default class SmartComponent extends Component {
  constructor(props) {
    super(props)
    this.state = { piyo: 0 }
  }
  piyoOperator() {
    // operate piyo
    const { hoge, fuga } = this.props
    this.setState({
      ...this.state,
      piyo: hoge + fuga
  }
  handleClick(e) {
    this.piyoOperator()
  }
  render() {
    return (
      // ビュー
      <PresentationComponent {...this.props} {...this.state}
        onClick={this.handleClick.bind(this)}
        onClickHoge={this.props.onClickHoge.bind(this)}
        onClickFuga={this.props.onClickFuga.bind(this)}
      />
    ))
  }
}

// 親コンポーネントで

import connectHogeDecorator from './smart-component'
import connectFugaDecorator from './smart-component'
import SmartComponent from './smart-component'

class ParentComponent extends Component {
  render() {
    return connectHogeDecorator(connectFugaDecorator(
      <SmartComponent {...this.props} />
    ))
  }
}

※概念コードなので動作確認はしていません。

方法は単純で、関数の引数をコンポーネントにして、そのコンポーネントをrenderするコンポーネントを返す、その時にstateやpropsを直にassignするという形です。誤ってメンバーを上書きする可能性はありますが…そのへんはよしなに。

これで、分割したコンポーネントのstateやpropsをベースコンポーネントにきれいに集約できそうです。

コンポーネントの入れ子で実現してみる

試しに、コンポーネントの入れ子で実現すると、cloneElementまわりが煩雑になりがちで、不要なdivタグも出てきます。Componentを返す関数を使うほうがdivタグは必要なときだけ入れられますし、renderメソッドも単純です。


'use strict'

import React, { Component, cloneElement } from 'react'
import PresentationComponent from './presentation-component'

// hogeについての責務
export default class HogeDecorator extends Component {
  constructor(props) {
    super(props)
    this.state = { hoge: 0 }
  }
  hogeOperator() {
    // operate hoge
  }
  handleClickHoge(e) {
    this.hogeOperator()
  }
  render() {
    return (
      <div>
        {this.props.children && cloneElement(this.props.children, {
          ...this.state,
          ...this.props,
          onClickHoge: this.handleClickHoge.bind(this)
        })}
      </div>
    )
  }
}

// fugaについての責務
export default class FugaDecorator extends Component {
  constructor(props) {
    super(props)
    this.state = { fuga: 1 }
  }
  fugaOperator() {
    // operate fuga
  }
  handleClickFuga(e) {
    this.fugaOperator()
  }
  render() {
    return (
      <div>
        {this.props.children && cloneElement(this.props.children, {
          ...this.state,
          ...this.props,
          onClickFuga: this.handleClickFuga.bind(this)
        })}
      </div>
    )
  }
}

// ベースコンポーネント
export default class SmartComponent extends Component {
  constructor(props) {
    super(props)
    this.state = { piyo: 0 }
  }
  piyoOperator() {
    // operate piyo
    const { hoge, fuga } = this.props
    this.setState({
      ...this.state,
      piyo: hoge + fuga
  }
  handleClick(e) {
    this.piyoOperator()
  }
  render() {
    return (
      // ビュー
      <PresentationComponent {...this.props} {...this.state}
        onClick={this.handleClick.bind(this)}
        onClickHoge={this.props.onClickHoge.bind(this)}
        onClickFuga={this.props.onClickFuga.bind(this)}
      />
    ))
  }
}

// 親コンポーネントで

import HogeDecorator from './smart-component'
import FugaDecorator from './smart-component'
import SmartComponent from './smart-component'

class ParentComponent extends Component {
  render() {
    return (
      <HogeDecorator {...this.props}>
        <FugaDecorator>
          <SmartComponent />
        </FugaDecorator>
      </HogeDecorator>
    )
  }
}

※概念コードなので動作確認はしていません。

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
3