FlowのGetting 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を書いてみます。
// @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に怒られます。
次に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が指定されている、と怒られます。
以下の様にすればエラーが消えます。
<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で関数を記載することです。
上記の場合、PropsInput
とPropsOutput
の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 }>
というように表現しています。