Cover image for ReactのHigher order component(HOC)をTypeScriptで作る
reacttypescript

ReactのHigher order component(HOC)をTypeScriptで作る

February 20, 2018

12 min read

mitsuruogMitsuru Ogawa

React の Higher order component(以下、HOC)を TypeScript で作る方法の紹介です。 作り方を探したところ、こちらの記事がほぼ完璧だと思ったので、本人に許可をもらってゆるく翻訳しながら紹介することにしました。

(完全な翻訳を目指しているわけではないので、細かいニュアンスまで気になる方は英語の記事を直接読んでください。あと少しコードは自分好みに変えてます。)

本人によると、この記事は HOC の作り方について同僚とディスカッションした内容をまとめたものだそうです。同僚大事。

HOC とは何か?

公式ドキュメントによると、HOC とは

a higher-order component is a function that takes a component and returns a new component (HOC とは、コンポーネントをもらって新しいコンポーネントを返す関数です。)

HOC は機能横断的な機能を抽出するために利用され、複数のコンポーネントを一箇所にまとめることで、コードの重複を減らすことができます。 ちなみに、もっとも有名な HOC はreact-reduxconnectだそうです。

これから学ぶこと

この記事では、clickCountHOCという HOC を作成します。

clickCountHOCはクリック数を子コンポーネント(wrapped コンポーネントと呼ぶ)の props に渡します。さらにクリック数を表示して、style prop を使ってスタイリングすることが可能です。 そして最後に、クリックした時にconsole.logが出力できるよう、設定できるようにします。

これらの要素は、HOC の全ての側面を可能な限りシンプルに説明するために選ばれています。

Props

まず、HOC を作成する際には3 種類の Propsについて考える必要があります。

  • OriginalPropsは、ラップされるコンポーネントが持つオリジナルの Propsです。HOC はこれらの内容をを全く知りません。
  • ExternalPropsは、HOC によって定義された Props です。これらはラップされるコンポーネントには渡されません。
  • InjectedPropsは、HOC がラップされるコンポーネントに追加する Propsです。これらは基本的に、HOC の State とExternalPropsを合成したものです。

これら 3 つの prop の関連は次のような図で表すことができます。

この図でわかるように、resulting component(HOC で新しく作られたコンポーネント)の Props はOriginalProps & ExternalProps(2 つを合成したもの)です。

今回の例のclickCountHOCProps はこのようになります。

interface ExternalProps {
  style?: React.CSSProperties;
}

export interface InjectedProps {
  clickCount?: number;
}

InjectedPropsは HOC を利用する際に使うため、export する必要があります(後述)。 そして State はシンプルで、ただクリックカウントがあるだけです。

interface State {
  clickCount: number;
}

Options

最初に言ったように、HOC とは、コンポーネントをもらって新しいコンポーネントを返す関数です。

(これを簡単に図示すると)

(Component) => Component;

しかし、多くの HOC はオプションの設定をもらって HOC を返す、カリー化された HOC ファクトリーのような形式になっています。(react-reduxもそう)

(options) => (Component) => Component;

これらのオプションは HOC 自体を変更するために利用される静的な定義体です。 また、これらは State や Props に対するアクセスはできず、HOC ファクトリーが呼び出された時に一度だけ評価されることに注意してください。

もし、この場所から Props や State を操作する必要がある場合、唯一の方法は、関数としてオプションを指定することです。引数として Props や State を受け取ることができます。

clickCountHOCのオプションはシンプルで、クリックした時にメッセージをコンソールに出力するかを指示するためのフラグです。

interface Options {
  debug?: boolean;
}

全てを一箇所にまとめる

必要な全ての Props を定義し終えたら、HOC を書く事ができます。

export const clickCountHOC =
  ({ debug = false }: Options = {}) =>
  <OriginalProps extends {}>(
    WrappedComponent: React.ComponentType<OriginalProps & InjectedProps>
  ) => {
    // body
  };

一目見ただけでは、少し複雑に見えるので、パーツごとに分解してみましょう。

({ debug = false }: Options = {}) =>

最初の行は一つの引数をもらうラムダ関数で、ES6 の destructuring 構文を使って、デフォルト値を持つキーとして分解されています。(今回の場合、キーはdebug) これにより呼び出し元が、この関数を引数なし1 つのOptionsを引数として呼び出せるようになります。そして一部のキーが渡されなかった場合、内部的にデフォルト値が利用されます。

<OriginalProps extends {}>(
  WrappedComponent: React.ComponentType<OriginalProps & InjectedProps>
) => {
  // body
};

2 つめは、1 つの型引数OriginalPropsを伴った generic ラムダ関数です。 extends {}は、HOC が JSX タグではなくラムダ関数であることを指し示すための、決まり文句のようなものです。

このラムダ関数はWrappedComponentというただ 1 つの引数をとり、2 つの型になる可能性があります。(大文字から始まることに注意してください。これは意図的で、後ろの方に理由が書いてあります。)

(注意)原文では、React.ComponentClassReact.StatelessComponentの 2 つを使っていますが、コメントでよりシンプルなやり方としてReact.ComponentTypeが提示されていたので、これを使っています。

  • React.ComponentType - React.ComponentClassReact.StatelessComponent を合成した型です。

この Props の型は上の図にある、2 つの型がラップされるコンポーネントに渡される場所に対応しています。 これで基本的な構文ができたので、あとは中身を作っていくだけです。

export const clickCountHOC =
  ({ debug = false }: Options = {}) =>
  <OriginalProps extends {}>(
    WrappedComponent: React.ComponentType<OriginalProps & InjectedProps>
  ) => {
    type ResultProps = OriginalProps & ExternalProps;

    return class ClickCountHOC extends React.Component<ResultProps, State> {
      static displayName = `ClickCountHOC(${WrappedComponent.displayName})`;

      constructor(props: ResultProps) {
        super(props);

        this.state = {
          clickCount: 0,
        };

        this.onClick = this.onClick.bind(this);
      }

      public render(): JSX.Element {
        return (
          <div onClick={this.onClick} style={this.props.style}>
            <span>Clicked {this.state.clickCount} times</span>
            <WrappedComponent {...this.props} {...this.state} />
          </div>
        );
      }

      private onClick() {
        if (debug) {
          console.debug("clicked");
        }
        this.setState({ clickCount: this.state.clickCount + 1 });
      }
    };
  };

まず最初に、resulting component の props の型(上の例ではResultProps)を定義します。 単純にOriginalProps & ExternalPropsとします。

次に、この Props 型を持つ resulting component のクラスを作成します。state にも適切なものを設定してください。

静的なプロパティdisplayNameを定義します。これは(ReactDev tool などで)デバックする際に、ラップされたコンポーネント名を知るために役立つものです。そして、state を初期化するシンプルなコンストラクタを定義します。

handleClickはクリックカウントを計算するための関数で、debugが有効な場合にメッセージをコンソールに出力します。

最後は、render関数です。styleと Click ハンドラを持つdivタグです。div の中のspanはクリックカウントを表示します。 これがWrappedComponentが大文字で始める理由です、そうでなければこのようにレンダリングできません。 OriginalPropsにあったもの全てと HOC の State にあるclickCountと一緒に渡されます。

HOC を使う

HOC の使い方について紹介しましょう。まずClickAreaというコンポーネントを作成して、これを HCO でラップします。

import { InjectedProps } from "./ClickCountHOC";

interface ClickAreaProps {}

const ClickArea = (props: ClickAreaProps & InjectedProps) => (
  <div>Click me!!</div>
);

export default ClickArea;

注意することは、この props の型はClickAreaProps(すなわちOriginalProps)とInjectedPropsの合成ということです。こうすることで、HOC とラップされたコンポーネントから props を使う事ができます。

最後にラップした 2 つのコンポーネントを作成します。(1 つはデバック機能付き)

import ClickArea from "./ClickArea";
import { clickCountHOC } from "./ClickCountHOC";

export interface HelloProps {}

const Wrapped1 = clickCountHOC()(ClickArea);
const Wrapped2 = clickCountHOC({ debug: true })(ClickArea);

export class Hello extends React.Component<HelloProps, {}> {
  render() {
    return (
      <div>
        <h1>Here is a simple example with HOC</h1>
        <Wrapped1 style={{ padding: 10 }} />
        <Wrapped2 style={{ padding: 10, background: "gray" }} />
      </div>
    );
  }
}

このように好きなコンポーネントと一緒に使う事ができ、TypeScript の型チェックの恩恵も受けることができます。

以上

まとめ

HOC を TypeScript で作る際の簡単なサンプルと説明でした。

英語の記事に方には、HOC のテンプレートもあるので、ぜひチェックしてみてください。

記事の中のコードは全てこちらのリポジトリで見ることができます。

HOC の作り方は最初難しいので、このような素晴らしい記事に出会えて本当によかったです。ありがとう!Dan

Making HOC with TypeScript is complex work at the first glance. but I am so happy to find such a great article!! Thanks Dan. I love you!!