LoginSignup
37
35

More than 5 years have passed since last update.

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

Last updated at Posted at 2015-05-12

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

Mixins Are Dead. Long Live Composition — Medium

Gunosy React Meetupkoba04さんのLTで紹介されていたのを今更調べたのでまとめる。

mixinのおさらい

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

// 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;
}

こういう感じで使う。

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が提供していたライフサイクルのフックを実現する方法。

/**
 * 第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;
};

こういう感じで使う

// 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やDOM操作をするようなMixinはHigher-Order Componentでは実装が難しいORできない。
記述量もMixinよりも多めな感じになる。

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

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

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

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

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

// 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} />;
  }}
/>

実装を見ると、子ComponetをCloneしてpropsを変えるみたいな実装になっている。

まとめ

37
35
0

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
37
35