v0.13からのES6 Class記法でmixinが使えない問題を解決する方法としてHigher-Order Componentsという方法がある。
Mixins Are Dead. Long Live Composition — Medium
Gunosy React Meetupのkoba04さんの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を変えるみたいな実装になっている。
まとめ
- mixinは今後、ES6 Class記法に実装される可能性もあるが、問題もある
- Higher-Orderを使うとMixin・Component間が疎結合になり、シンプルでいい
- mixinを完全に置き換えることができるわけではないが、なんとかなりそうなきがする。