LoginSignup
21
12

More than 5 years have passed since last update.

Flowで型を指定しながらHOCを書く

Last updated at Posted at 2017-02-08

注意: この記事はかなり昔の Flow をもとに書かれました。当時と比べ Flow は特に React まわりで大きな変更が入っており、おそらくこの記事の賞味期限は切れていると思います。


React で大きなソフトウェアを書いている時に、複数のコンポーネントに共通する処理を抽象化するためのプラクティスに Higher-Order Components (HOC)というものがあります。HOCについて一言で言うなら「React コンポーネントを受け取って、それをラップした新しい React コンポーネントを返す関数」です。

HOC では引数に受け取るコンポーネントに特定のインタフェースを期待することが多いと思います。こう言う時こそ flow を使って引数の型を強制したくなりますね。

期待するのは例えば「Promise<*> を返す update メソッドを持っていること」などですが、その一方でその引数はコンポーネントでもなければなりません。このような型は次のように書くことができます。

// @flow
interface Updatable {
  update(): Promise<*>;
}

type UpdatableComponent = Class<Updatable> & ReactClass<*>;

なぜ Class<Updatable> などとしているかと言うと、マウントした後にそのインスタンスを扱うことを意図しているからです。

全体を合わせると HOC の型はこんな感じで書くことができます。

この HOC はコンポーネントを componentDidMount の度に update() を実行するものに変更します。ただし、 underscore の throttle を使って interval に 1 度しか更新しません。また isMounted is an Antipattern - React Blog で紹介されている makeCancelable 関数を使ってアンマウント時に自動的にリクエストを停止し、メモリリークを防ぎます。

// @flow
import {Component} from "react";
import {throttle} from "underscore";

import {makeCancelable} from "./pat/to/file";

interface Updatable {
  update(): Promise<*>;
}

export default function withThrottledUpdate(
  WrappedComponent: Class<Updatable> & ReactClass<*>
): ReactClass<*> {
  return class extends Component {
    instance: ?Updatable;
    throttledUpdate: ?Function;

    request: ?{
      promise: Promise<*>;
      cancel: Function;
    }

    props: {
      interval: number;
      leading: boolean;
    }

    update(): Promise<*> {
      if (this.instance) {
        this.request = this.instance.update();
      }
    }

    proc(wrappedComponentInstance: Updatable) {
      this.instance = wrappedComponentInstance;

      this.throttledUpdate = throttle(
        this.update.bind(this),
        this.props.interval,
        { leading: this.props.leading }
      );
    }

    componentDidMount() {
      this.update();
    }

    componentDidUpdate() {
      if (this.throttledUpdate) {
        this.throttledUpdate();
      }
    }

    componentWillUnmount() {
      if (this.request) {
        this.request.cancel();
      }
    }

    render() {
      const { interval, leading, ...ThroughProps } = this.props;
      return <WrappedComponent ref={this.proc.bind(this)} {...passThroughProps} />;
    }
  }
}
21
12
0

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
21
12