Help us understand the problem. What is going on with this article?

React の Higher-order Components の利用方法

More than 1 year has passed since last update.

React の Higher-order Components の利用方法

by numanomanu
1 / 38

前書き

この資料は2016/12/15 に行われた【ランサーズ×Mozilla×freee】React実践!勉強会で発表した際の資料を改修したものです。
(qitta に投稿するのが初めての場合、2MBまでしか画像をアップできないので、編集してます)
間違いなどがあれば、コメントや編集リクエストなどをいただければありがたいです。


目次

  • 対象読者
  • HOC とは何か?
  • HOC の基礎
  • 実際に利用されているライブラリについて
  • HOC の具体的な実装例

対象読者

  • Reactを利用してアプリケーションを作成したことがある人
  • ReduxやFluxなどのフレームワークを利用したことがある人

HOC とは何か?

A higher-order component is just a function that takes an existing component and returns another component that wraps it.
(引用:Mixins Are Dead. Long Live Composition)

  • コンポーネントを引数にして、カスタマイズしたコンポーネントを返す関数のこと
  • 既存のコンポーネントに新しい機能を追加することが可能

HOCの基礎

sebmarkbage/Enhance.jsの例

Enhance.js
import { Component } from "React";
// 関数の引数にコンポーネントを受け取る
// function Enhance(ComposedComponent) { return xxx } と同じ
export var Enhance = ComposedComponent => class extends Component {
  constructor() {
    this.state = { data: null };
  }
  componentDidMount() {
    this.setState({ data: 'Hello' });
  }
  render() {
    // 引数で受け取った ComposedComponent に props を渡して返す
    return <ComposedComponent {...this.props} data={this.state.data} />;
  }
};
  • 引数に受け取った ComposedComponent に props などをマージして return

要点を抜き出すと

image


利用する側の実装

HigherOrderComponent.js
import { Enhance } from "./Enhance";

class MyComponent {
  render() {
    // MyCompoent で Enhancer の data を参照できるようになった
    if (!this.data) return <div>Waiting...</div>;
    return <div>{this.data}</div>;
  }
}

export default Enhance(MyComponent); // Enhanced component
  • Enhance(MyComponent) で MyComponent に HOC を適応
  • 関数でラップすることで、 data というプロパティが新たに追加された

要点を抜き出すと

image


HOCの基礎:イメージ

image
- コンポーネントに関数を適応し、機能が合成されたコンポーネントを返す
- propsを新しく加えたり、ライフサイクルメソッドを追加することも可能


最近よく見かける HOC を利用しているライブラリとその実装


material-uiでの例

  • Google Material Design を react の component で利用出来るライブラリ image

  • material-ui / themes
  • テーマの色設定などは、muiTheme という形で1つのファイルで管理している
  • textColor などの定義を、自作のコンポーネントでも使いたい場合・・・
  • muiThemeable という HOC を呼び出す
muiTheme.js
import { cyan500, pinkA200, darkBlack } from '../colors';
import spacing from '../spacing';

export default {
  spacing: spacing,
  fontFamily: 'Roboto, sans-serif',
  palette: {
    primary1Color: cyan500,
    accent1Color: pinkA200,
    textColor: darkBlack,
    // 省略...
  },
};

material-ui / muiThemeable

  • マテリアルUIの textColor などの定義を、自作のコンポーネント内で呼び出す
muiThemeableを使ってコンポーネント内でThemeを呼び出す
import React from 'react';
import muiThemeable from 'material-ui/styles/muiThemeable';

const MyComponent = (props) => (
  <span style={{color: props.muiTheme.palette.textColor}}>
    Hello World!
  </span>
);

export default muiThemeable()(MyComponent);
  • muiThemeable()(MyComponent)の部分がHOC
  • props に muiTheme.palette.textColor が追加されて、自作のコンポーネント内でも扱えるようになった

muiThemeable.js
import React, {PropTypes} from 'react';
import getMuiTheme from './getMuiTheme';

export default function muiThemeable() {
  // 省略...
  return (Component) => {
    const MuiComponent = (props, context) => {
      const {muiTheme = getMuiTheme()} = context;

      return <Component muiTheme={muiTheme} {...props} />;
    };
    // 省略...
    return MuiComponent;
  };
}

Component を引数にとって、 muiTheme という props を持った MuiComponent という新しいコンポーネントを return している


形としてみると

image


ReduxForm での例

  • ReduxForm
  • 入力フォームなどを redux のフローで扱ってくれるライブラリ image

ReduxForm でHOCを利用する

簡単なプロフィールフォームの例

import React, { Component } from 'react';
import { reduxForm, Field } from 'redux-form';

class ProfileForm extends Component {
  render() {
    const { handleSubmit } = this.props;
    return (
      <form onSubmit={handleSubmit}>
        <Field name="firstName" component="input" type="text"/>
      </form>
    );
  }
}

ProfileForm = reduxForm({
  form: 'profile'
})(ProfileForm);

export default ProfileForm;
reduxForm({})(ラップされるコンポーネント)

という構成。


ReduxForm でHOCを利用する

image


import { Component, createElement } from 'react'
// 省略...
const createReduxForm = structure =>
    // 省略...
    return WrappedComponent => {
        // 省略...
        return class ReduxForm extends Component {
            render() {
                return createElement(WrappedComponent, {})
            }
        }
    }
}
export default createReduxForm

要点を抜き出すと

image


ここまでのまとめ1

React 関連のライブラリを使っていて

Enhancer(WrappedComponent)

というような、コンポーネントを引数に取る構成の記述を見かけた場合は HOC の実装を使っているのではないかと考えると理解しやすい。


ここまでのまとめ2

  1. 関数がコンポーネントを引数にとって
  2. 何らかの処理を加えて
    • 新しい props の注入
    • 共通のライフサイクル処理
  3. 引数にとったコンポーネントに合成して返す

案外、 HOC の実装はパッと見で複雑でも、元のコンポーネントに新しく props を注入したりしているだけかもしれません。


HOC の 2 つのパターン

React Higher Order Components in depthの記事によると、HOC には2つのパターンがあります

  1. 先ほどまでの props を新しく追加する例は Props Proxy
  2. PropsProxy 以外には Inheritance Inversion

実際の実装を見ながら順に説明していきます。


HOC の具体的な実装例1

  • ボタンのバリエーションを HOC で表現する
  • 以下のようなプライマリカラーの効いたボタンを作りたいとき image

HOC の具体的な実装例1

  • createPrimaryButton という関数で、HOCを作成
  • material-ui の RaisedButton コンポーネントを拡張
プライマリボタンを作成する例
import React, { Component } from 'react';
import RaisedButton from 'material-ui/RaisedButton';

function createPrimaryButton(WrappedComponent) {

    return class designedButtonComponent extends Component {

        render() {
            return (<WrappedComponent {...this.props} primary={true} />);
        }
    }
}
export const PrimaryButton = createPrimaryButton(RaisedButton);

HOC の具体的な実装例1: HOC を作成

image

  • コンポーネントに与えられた props をそのまま受け渡しつつ、新しく primary={true} という props を追加する

HOC の具体的な実装例1: 呼び出す側

  • PrimaryButton という名前で呼び出すことで、利用者側も理解しやすいコードになる
  • material-ui の label などのプロパティも同時に利用出来る
ボタンの利用例
import { PrimaryButton } from './buttons';
export const renderSomePageWithButton = buttonAction =>
<div>
    <PrimaryButton
        label={'プライマリボタン'} //ボタンに表示するテキスト
        onTouchTap={buttonAction}
    />
</div>

HOC の具体的な実装例1

  • createButton に引数をつけて、バリエーションを増やす
  • 先ほどの HOC に color, size, shape などの引数を追加
buttons.js
import React, { Component }from 'react';
import RaisedButton from 'material-ui/RaisedButton';
import { buttonColor, buttonSize, buttonShape } from './button_styles';

function createButton(WrappedComponent, color, size, shape) {
    return class designedButtonComponent extends Component {
        render() {
            return (<WrappedComponent
                {...this.props}
                {...buttonColor[color]}
                {...buttonSize[size]}
                {...buttonShape[shape]}
            />);
        }
    }
}
export const PrimaryButtonFullWidth = createButton(RaisedButton, 'primary', 'fullWidth', 'original');
export const SecondaryButtonRounded = createButton(RaisedButton, 'secondary', 'original', 'round');
export const AccentButtonHalfWidth = createButton(RaisedButton, 'accent', 'halfWidth', 'original');

HOC の具体的な実装例1: 呼び出す側

ボタンのバリエーションの利用例
import {
    PrimaryButtonFullWidth,
    AccentButtonHalfWidth,
    NSecondaryButtonRounded } from './buttons';
export const renderSomePageWithButton = buttonAction =>
<div>
    <NPrimaryButtonFullWidth label={'プライマリボタン幅MAX'} />
    <NSecondaryButtonRounded label={'角丸セカンダリボタン'} />
    <NAccentButtonHalfWidth label={'アクセントボタン幅半分'} />
</div>

HOCを使ったコンポーネントのパターン実装のメリット

  • HOC を用いれば、コンポーネントに新しく props を追加できる
  • PrimaryButtonFullWidthなど、限定的な名前を持たせることで、コードがリーダブルになる
  • FlowTypeなどで型を持たせると、さらにリーダブルになる
// @flow
type ColorType =
    'primary'
    | 'secondary'
    | 'accent'
;
function createButton(WrappedComponent: any, color:ColorType) {...}

 HOC の具体的な実装例2

  • 閲覧するのにログインが必要なコンポーネントをHOCで制御
  • Redux で、 session.userId でログインユーザを管理している前提
  • loggedInRequired()というHOCを作ってみる

 HOC の具体的な実装例2: 利用する側

  • ログインが必要な箇所は、HOCでラップしてあげる
  • 「ログインしていません」のようなダイアログを出したい
ラップされる側のコンポーネントの利用イメージ
import loggedInRequired from '../loggedInRequired.js'
// 省略...
class ProjectContainer extends Component {
// 省略...
}
export default loggedInRequired(
    connect(mapStateToProps, mapDispatchToProps)
    (ProjectContainer)
);

 HOC の具体的な実装例2: HOCを作成する側

export default function loggedInRequired(WrappedComponent) {
    class loggedInRequiredComponent extends WrappedComponent {

        componentWillMount() {
            // react-redux のステートには this.store.getState() でアクセスできます
            if (!this.store.getState().session.userId) {
                // ログインしていないユーザが見た時のアクションを書く
            }
        }
        render() {
            if (!this.store.getState().session.userId) {
                // ログインしていないユーザが閲覧したらレンダリングしない
                return null;
            }
            return super.render();
        }
    }
    return loggedInRequiredComponent;
}
  • さっきまでと異なるのは、 WrappedComponent を return するのではなく、 extends している点
  • Inheritance Inversion というパターン
  • react-redux のステートには this.state.storeState でアクセスできます -> this.store.getState() に修正しました。(react-redux v5系以降)

HOC の具体的な実装例2で行っていること

ラップしたコンポーネント自身を extends している。(Inheritance Inversion)

  • HOCのライフサイクルメソッドを、ラップしたコンポーネントに適応
  • HOCから、ラップしたコンポーネントの state や props に thisでアクセス
  • 条件によって、レンダリングしないように、 render をハイジャックできる

ログインが必要なコンポーネントは、このHOCを利用すれば良いため、他のコンポーネント内にif文などを書かなくて良くなり、コンポーネント自身の責務に集中出来る。


おまけ:複数のHOCを合成して使うテクニック

コンポーネントを複数のHOCでラップしたい場合
- loggedInRequired(connect(mapStateToProps, mapDispatchToProps)(ProfileForm)) のように書くのか?
- redux の compose や lodash.js の _.compose を利用するとスッキリかけます

import { compose } from 'redux';
import { reduxForm } from 'redux-form';

class ProfileForm extends Component {
...
}
ProfileForm = compose(
    loggedInRequired,
    connect(mapStateToProps, mapDispatchToProps),
    reduxForm({ form: 'ProfileForm' })
)(ProfileFormContainer)

export default ProfileForm

まとめ

  • HOC はコンポーネントを引数にして、カスタマイズしたコンポーネントを返す関数のこと
  • material-ui や redux-form など、様々なライブラリで利用されている
  • 組み合わせることで、既存のコンポーネントに新しい機能を追加することが可能
  • アプリケーション内で共通に利用する機能や、パーツをまとめるのに便利
  • 慣れればコードを読みやすくかける

参考資料

numanomanu
「つくり続けなければ死ぬ」 https://note.mu/numanomanu
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away