React
HOC

ReactのHOCについて

Reactの機能の一つ、HOC(Higher-Order Component)についての説明をする。

これの理解により、適切なパフォーマンスチューリングや、記述量の削減に大きく貢献できる。


HOCとは?

以下のようなコードについて考える。


A.jsx

class A extends React.Component{

render(){
return (
<div>Hello, world</div>
);
}
}

export default A;



B.jsx

import A from 'A.jsx';

class B extends React.Component{
render(){
return (
<A />
);
}
}


ここで、<B />の出力結果は、当然Hello, worldになる。

ここで、以下のような関数を用意する。

const sayHi = WrappedCompoent => {

return class extends WrappedCompoent{
componentWillMount(){
console.log('Hi!');
}
}
}

これを、Aに適用してみる。


A.jsx

class A extends React.Component{

render(){
return (
<div>Hello, world</div>
);
}
}

export default sayHi(A);


ここで、<B />を評価すると、コンソールにHi!と表示されるようになる。

sayHi関数について考えてみる。

sayHi関数は、コンポーネントを受け取って、コンポーネントを返す関数となっている。

渡されたコンポーネントクラスを継承し、メソッドをオーバーライドするクラスを返すことによって、「機能を拡張」している。

このように、「コンポーネントを受け取り、コンポーネントを返す関数」を、HOC(Higher-Order Component)という。

(「『コンポーネントを受け取り、コンポーネントを返す関数』により、拡張されたコンポーネント」の方をHOCと呼ぶ方が語彙的には適切だと思うが、公式ドキュメントの方では上のような定義になっている。ここでは便宜上、公式の方の定義を参照する。)

なお、sayHi関数の機能は、次のようにして実現することもできる。

const say = message => WrappedCompoent => {

return class extends WrappedCompoent{
componentWillMount(){
console.log(message);
}
}
}


A.jsx

...

export default say('Hi!')(A)

このように、HOCは高階関数として提供されることが多い。

詳しくは、公式ドキュメント


コンポーネントコンポジションとの違い

コンポーネントの機能の拡張には、コンポーネントコンポジション(OOPでいうところのDecoratorパターン)が使われることが多い。

例えば、文字のリストを表示する<TextList />に、スクロールバーを実装する場合は次のようになる。

<ScrollBar>

<TextList />
<ScrollBar />

ここで、<ScrollBar /><TextList />の実装に依存しない(カプセル化を壊さない)ので、他のコンポーネントにもすぐに容易に実装できる。

これに対し、HOCはクラス継承を行うことにより実現できる、つまりHOCの対象となるコンポーネントの実装に依存する(カプセル化を壊す)

拡張対象のコンポーネントの実装に依存するか、しないかでコンポジションとHOCは使い分ける必要がある。

拡張性を高めるため、基本的にはコンポジションの方を優先する。

参考: Composition vs Inheritance


Recomposeの導入

HOC周りのヘルパメソッドを提供する、Recomposeライブラリを導入。

これのわかりやすい日本語のドキュメントはここ

例えば、compose関数なら、

今まで以下のようになっていたものを、

HOC1(HOC2(HOC3(Component)))

次のようにできる。

const Enhance = compose(

HOC1,
HOC2,
HOC3
);

Enhance(Component)


HOCの使用例


React-Redux

ReactとReduxを組み合わせて使う場合、

Reactの各コンポーネントのpropsと、Reduxのstate (ReactのStateと混同しないように!)action creator を繋げる必要がある。

ここで、

- Reduxのstatepropsに反映する mapStateToProps 関数

- Reduxのaction creatorpropsに反映する mapDispatchToProps関数

- それらを繋げるconnect関数

を用意し、それらを組み合わせてHOCとして適用する必要がある。

例えば、<A />に対し、Reduxのstateaction creatorを繋げる場合は、次のようになる。

connect(mapStateToProps, mapDispatchToProps)(A)


mapStateToProps

mapStateToPropsReduxのstateを受け取り、propsに追加したいstateを返す関数である。

例えば、state内のgenderpropsに反映する場合は、mapStateToPropsは次のようになる

const mapStateToProps = state => ({

gender: state.gender
});

これにより、コンポーネント内ではthis.props.genderとしてアクセスできるようになる。

ところで、プロジェクト内の既存のコードだと、次のようになっている部分が多い

const mapStateToProps = state => {

return state;
};

しかし、これだとComponent側のpropsにstateの情報が全て反映されてしまい、関係のないstateが変わるたびにそのコンポーネントの描画処理が行われるため、パフォーマンスが悪化してしまう。

なので、上のように必要なものだけpropsに反映するようにする必要がある。

なお、mapStateToPropsを二重で適用したりする際などに、現在のpropsを参照する場合は、第二引数ownPropsを宣言する。

const mapStateToProps = (state, ownProps) => ({

...ownProps,
gender: state.gender
});


mapDispatchToProps

mapDispatchToPropsは、Reduxのdispactherを受け取り、propsに追加したいaction creatorを返す関数である。

setGenderをpropsに渡す場合は、mapDispatchToPropsは次のようになる

import  { setGender } from 'actions.jsx';

const mapDispatchToProps = dispatch => {
return {
actions: {
setGender: gender => dispatch(setGender(gender))
}
};
};

これは、bindActionCreatorsというヘルパ関数を使うことにより、次のように書ける。

const mapDispatchToProps = dispatch => {

return {
actions: bindActionCreators({ setGender }, dispatch)
};
};

これにより、コンポーネント内ではthis.props.actions.setGenderとしてアクセスできる。

ところで、プロジェクト内の既存のコードだと、次のようになっている部分が多い

const mapDispatchToProps = dispatch => {

return {
actions: bindActionCreators(Actions, dispatch)
};
};

これも同じく、不要なaction creatorをpropsに渡してしまっているので、パフォーマンスの低下に繋がる。

なお、mapStateToPropsを二重で適用したりする際などに、現在のpropsを参照する場合は、第二引数ownPropsを宣言する。

const mapDispatchToProps = (dispatch, ownProps) => {

return {
actions: {
...ownProps.actions,
...bindActionCreators({ setGender }, dispatch);
};
};
}

参考: 5 way to Connect Redux Actions