LoginSignup
7

More than 5 years have passed since last update.

ReactのHOCについて

Posted at

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

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
7