flow
React

FlowのGetting Started(React)

FlowGetting Startedを一通りさらった際のメモです。

Setup Flow with React

今回はCreate React AppでReactアプリの雛形を作ってしまいます。
これでアプリの雛形を作ってしまうと、Babelに対する設定も不要(Create React App already supports Flow by default)になるので、ひとまずFlowの機能を試すための最短手順になると思います。

公式に記載されているまんまですが、一応手順も貼っておきます。

create-react-app my-app && cd my-app
yarn add --dev flow-bin
yarn run flow init

Visual Studio Codeのセットアップ

Getting Startedの手順からは外れますが、私はエディタにVS Codeを使っているので、FlowのExtensionをインストールしました。
以下を参考にさせていただきました。

Components

冒頭はPropTypesを使った例が示されていて、それがFlowだと「こうなりますよ」という体で書かれています。
生成したアプリでFlowの挙動を確認するため、以下のようにMyComponentを書いてみます。

MyComponent
// @flow
import * as React from 'react';

type MyProps = {
  foo: number,
  bar?: string,
};

class MyComponent extends React.Component<MyProps> {
  render() {
    this.props.doesNotExist; // Error! You did not define a `doesNotExist` prop.

    return <div>{this.props.bar}</div>;
  }
}

export default MyComponent;

コメントにもあるように、doesNotExistというプロパティはMyPropsというTypeには存在しないのでFlowに怒られます。

スクリーンショット 2018-03-16 16.39.12.jpg

次にAppを以下の様に書き換えます。

App
// @flow
import React, { Component } from "react";
import logo from "./logo.svg";
import "./App.css";
import MyComponent from "./MyComponent";

class App extends Component<{}> {
  render() {
    return (
      <div className="App">
        <MyComponent bar={42} />
      </div>
    );
  }
}

export default App;

fooは指定が必須であるのに指定されていない、と怒られます。
また、barはstringを要求しているのに、numberが指定されている、と怒られます。

スクリーンショット 2018-03-16 16.41.36.jpg

以下の様にすればエラーが消えます。

<MyComponent foo={42} />

この他に、Stateについても型定義できたり、デフォルト値を指定できたり、Functional Componentsでの書き方だったりの説明があります。

Event Handling

イベントハンドラの型安全。

// @flow
import * as React from "react";

class MyComponent extends React.Component<{}, { count: number }> {
  constructor() {
    super();
    this.state = {
      count: 0
    };
  }

  handleClick = (event: SyntheticEvent<HTMLButtonElement>) => {
    // To access your button instance use `event.currentTarget`.
    (event.currentTarget: HTMLButtonElement);

    this.setState(prevState => ({
      count: prevState.count + 1
    }));
  };

  render() {
    return (
      <div>
        <p>Count: {this.state.count}</p>
        <button onClick={this.handleClick}>Increment</button>
      </div>
    );
  }
}

export default MyComponent;

eventの変数の型をSyntheticEventとすることで、例えば、

event.srcElement

のように非推奨となった属性を参照しようとすると、ちゃんと怒られるようになります。

ref functions

// @flow
import * as React from "react";

class MyComponent extends React.Component<{}> {
  // The `?` here is important because you may not always have the instance.
  myButton: ?HTMLButtonElement;

  render() {
    return <button ref={button => (this.myButton = button)}>Toggle</button>;
  }
}

export default MyComponent;

イベントハンドラのevent引数に型を与えたように、DOM要素にも型安全を提供してくれるということです。
例えば、

if(this.myButton) this.myButton.disabled = true;

というようにHTMLButtonElementの定義に適合するような使い方であれば怒られませんが、

if(this.myButton) this.myButton.disabled = "aaa";

のように書くとFlowが指摘してくれます。
また、nullチェックもしないと、やはり指摘されます。
nullableとしているので、nullチェックなしのプロパティ参照に対して指摘してくれているわけです。

なお、ここでnullableにしている理由は以下の通りです。

The ? in ?HTMLButtonElement is important. In the example above the first argument to ref will be HTMLButtonElement | null as React will call your ref callback with null when the component unmounts.

Higher-order Components

このセクションについては未解決の問題があるんですが、さらった内容を記載します。

ここでは、recomposeのmapPropsに相当するような関数をどう記述するかということが説明されています。
残念ながら完全な実装が記載されていないので、私は以下のような関数を書きました。

function mapProps<PropsInput: {}, PropsOutput: {}>(
  mapperFn: PropsInput => PropsOutput
): (React.ComponentType<PropsOutput>) => React.ComponentType<PropsInput> {
  return WrappedComponent => {
    return class extends React.Component<PropsInput> {
      render() {
        return <WrappedComponent {...mapperFn(this.props)} />;
      }
    };
  };
}

ポイントはGeneric Typesで関数を記載することです。
上記の場合、PropsInputPropsOutputの2つの型引数をとる関数になっています。

マッパー関数(mapperFn)のType Annotationは、PropsInput => PropsOutputです。

mapPropsが返す関数は、React.ComponentType<PropsOutput>のコンポーネントを引数にとり、React.Component<PropsInput>のコンポーネント(Higher-order Components)を返します。

使う側は、これを満たすようにmapPropsを呼び出します。
まず、各コンポーネントが要求するPropsのTypeを定義しておきます。また、マッパー関数のTypeも定義しておきます。

type PropsA = {
  foo: number
};
type PropsB = {
  bar: number
};
type MapperPropFn = PropsA => PropsB;

mapPropsによりWrapされるコンポーネントは以下の通り。

function MyComponent({ bar }: PropsB) {
  return <div>{bar}</div>;
}

マッパーは以下の様になります。
引数に渡されるpropsにはfooがあり、返り値となるオブジェクトにはbarを含めます。

const propsMapper: MapperPropFn = props => {
  return { bar: props.foo + 1 };
};

mapPropsの呼び出しと、HOCの利用は以下のようになります。

const MyEnhancedComponent = mapProps(propsMapper)(MyComponent);
<MyEnhancedComponent foo={1} />;

mapPropsの呼び出し部分ですが、Flowのエラーになってしまいます。

Missing type annotation for `PropsInput`.

これをどう解消すればよいのか、色々調べてもわかりませんでした。

もう1つの例として、プロパティを追加する関数が紹介されています。

function injectProp<Props: {}>(
  WrappedComponent: React.ComponentType<Props>
): React.ComponentType<$Diff<Props, { foo?: number }>> {
  return function WrapperComponent(props: Props) {
    return <WrappedComponent {...props} foo={48} />;
  };
}

上の関数では、引数に渡されたコンポーネントにfooというプロパティを追加しています。
型引数は、前出のmapPropsとは異なり、Propsの1つだけを取ります。

引数に渡されるコンポーネントと、返すコンポーネントの差分を、

$Diff<Props, { foo?: number }>

というように表現しています。