Higher-Order Componetを使ってES6 Class記法でmixinっぽいことをする


v0.13からのES6 Class記法でmixinが使えない問題を解決する方法としてHigher-Order Componentsという方法がある。

[Mixins Are Dead. Long Live Composition — Medium](https://medium.com/@dan_abramov/mixins-are-dead-long-live-higher-order-components-94a0d2f9e750)

[Gunosy React Meetup](http://gunosy-frontend.connpass.com/event/12636/)の[koba04さんのLT](https://speakerdeck.com/koba04/react-dot-js-v0-dot-13-changes-and-beyond)で紹介されていたのを今更調べたのでまとめる。

## mixinのおさらい

FluxフレームワークでよくあるStoreとComponentをひもづけるためのMixinの例。

```js
// FluxフレームワークでよくあるStoreとComponentをひもづけるためのMixin
function StoreMixin(...stores) {
  var Mixin = {

    getInitialState() {
      // getStateFromStoresはComponent側に定義する
      return this.getStateFromStores(this.props);
    },

    // ライフサイクルメソッドにフックする
    // Mount時にaddListenerして、Umount時にremoveListener

    componentDidMount() {
      stores.forEach(store =>
        store.addChangeListener(this.handleStoresChanged)
      );

      this.setState(this.getStateFromStores(this.props));
    },

    componentWillUnmount() {
      stores.forEach(store =>
        store.removeChangeListener(this.handleStoresChanged)
      );
    },

    handleStoresChanged() {
      if (this.isMounted()) {
        this.setState(this.getStateFromStores(this.props));
      }
    }
  };

  return Mixin;
}
```

こういう感じで使う。

```js
var SomeComponent = React.createClass({
  // さっきのMixin
  // SomeStore更新時にSomeComponentのstateを更新する
  mixins: [StoreMixin(SomeStore)],

  propTypes: {
    userId: PropTypes.number.isRequired
  },

  // Mixinから呼ばれるメソッドを定義
  getStateFromStores(props) {
    return {
      user: UserStore.get(props.userId);
    }
  }

  render() {
    var { user } = this.state;
    return <div>{user ? user.name : 'Loading'}</div>;
  }
});
```

Mixinには、①ユーティリティ関数を複数のComponentで共有する、②ライフサイクルメソッドにフックしthis.stateを更新する、という大きく2つの用途があり、StoreMixinは②。

MixinとComponentで同じライフサイクルメソッドを定義している場合は、いい感じにマージしてどちらも実行してくれる。getInitialStateもキーがかぶっていなければマージされて設定される。

## Mixinのデメリット

ES6 Classで記法で使えない以外に、

- 上記の例の`getStateFromStores`メソッドのように、Component側にインターフェースを用意する必要がある場合、Mixinからそれを強制する方法がない。
- 複数のMixinで同じ名前のメソッドを定義しているとクラッシュする
	- 上記だと`mixins: [StoreMixin(A), StoreMixin(B)]`とすると、`handleStoresChanged`がかぶっているので例外を投げる
- `this.state`に値を追加するので複雑になる

などの問題がある。

## Higher-Order Componentで同じことをやる

Higher-Order ComponentsとはComponentを別のComponentでWrapして、そのWrapする側にライフサイクルのメソッドを定義することでmixinが提供していたライフサイクルのフックを実現する方法。


```js
/**
 * 第1引数に渡されたComponentと第2引数に渡されたStoreを紐付けて、
 * WrapしたComponentを返す
 *
 * storeからどういう風にデータを取り出すかはgetStateFromStoresで指定する
 */
function connectToStores(Component, stores, getStateFromStores) {

  // WrapするComponent
  class StoreConnection extends React.Component {
    getInitialState() {
      return getStateFromStores(this.props);
    }

    // ライフサイクルメソッドにフックする
    // Mount時にaddListenerして、Umount時にremoveListener

    componentDidMount() {
      stores.forEach(store =>
        store.addChangeListener(this.handleStoresChanged)
      );
    }

    componentWillUnmount() {
      stores.forEach(store =>
        store.removeChangeListener(this.handleStoresChanged)
      );
    }

    handleStoresChanged() {
      if (this.isMounted()) {
        this.setState(getStateFromStores(this.props));
      }
    }

    render() {
      // 引数で渡されたComponentにprops経由でデータを連携しrender
      return <Component {...this.props} {...this.state} />;
    }
  }

  return StoreConnection;
};
```

こういう感じで使う

```js
// WrapされるComponent
class SomeComponent extends React.Component { ... }

// 先ほどComponent側に定義していたメソッドは引数として明示的に渡す
var getStateFromStores = function (props) { ... };

// SomeComponentをWrapした新しいComponentを返す
const NewComponent = connectToStores(SomeComponent, [SomeStore], getStateFromStores);

// render
Reac.render(<NewComponent/>, document.body);
```

## Higher-Order Componentの良い点と制限と制限の回避

Higher-Order Componentの良い点は、こんな感じ。

- Mixin・Component間のデータの受け渡しがprops経由でシンプル
- 上記例のgetStateFromStoresのようなMixinがComponentに要求していたメソッドを切り離せるので、MixinとComponentが疎結合になる

制限としては、WrapするComponentのstateが見えない＆shouldComponentUpdateが定義できないので、[PureRenderMixin](https://facebook.github.io/react/docs/pure-render-mixin.html)やDOM操作をするようなMixinはHigher-Order Componentでは実装が難しいORできない。
記述量もMixinよりも多めな感じになる。

PureRenderMixinについては、PureComponentのようなBaseクラスを作ればいけるっぽい。

```js
class PureComponent extends React.Component {
	shouldComponentUpdate () {
		// stateの比較処理など
	}
}

```

WrapするComponent内部のDOMを取得したい場合は、v0.13からrefにcallback関数を渡せるようになったようなのでそれを使えば、Wrapper側からDOMを取得できるみたい。

```js
import React, { Component } from 'react';

class App extends Component {
  render () {
    // refにcallbackを渡す
    return <h1 ref={ this.props.refCallback }>Hello</h1>;
  }
}

function wrap (ChildComponent) {
  class Wrap extends Component {
    componentDidMount () {
      console.log('mounted!!!!!');
    }

    // refが引数に渡ってくるので
    // それをReact.findDOMNodeするとDOMが取れる
   setChildRef(ref) {
      console.log(React.findDOMNode(ref));
    }

    render () {
      return <ChildComponent refCallback={this.setChildRef.bind(this)}/>;
    }
  }

  return Wrap;
}

const NewComponent = wrapHello(App);
React.render(<NewComponent/>, document.body); // consoleにDOMが出力されるはず
```

## 他のアプローチ

### flummoxのFluxComponent

http://acdlite.github.io/flummox/docs/api/fluxcomponent

flummoxが提供している`<FluxComponent>`は、Higher-Order Componentと似ているが、関数ではなくComponentで実装されている。

```js
// InnerComponentにthis.props.posts, this.props.comments経由で
// Storeのデータが渡される
<FluxComponent connectToStores={{
  posts: store => ({
    post: store.getPost(this.props.post.id),
  }),
  comments: store => ({
    comments: store.getCommentsForPost(this.props.post.id),
  })
}}>
  <InnerComponent />
</FluxComponent>

// renderをカスタムできる
<FluxComponent
  connectToStores={{
    posts: store => ({
      post: store.getPost(this.props.postId),
    })
  }}
  render={storeState => {
    // Render whatever you want
    return <InnerComponent {...storeState} />;
  }}
/>
```

[実装](https://github.com/acdlite/flummox/blob/master/src/addons/FluxComponent.js#L63-L68)を見ると、子ComponetをCloneしてpropsを変えるみたいな実装になっている。


## まとめ

- mixinは今後、ES6 Class記法に実装される可能性もあるが、問題もある
	- [React0.13.0-beta.1でのComponentのES6 classでの継承とmixinが使えない理由](http://qiita.com/mizchi/items/ce9a128aa2a47804e083)
	- サポートされないっぽい
- Higher-Orderを使うとMixin・Component間が疎結合になり、シンプルでいい
- mixinを完全に置き換えることができるわけではないが、なんとかなりそうなきがする。