Flow&React-天生一对

React作为facebook的亲儿子,flow自然会鼎力配合React的工作,不需要任何额外的配置,flow已经很完整地支持React的类型系统。



好处

引入声明系统对React组件而言,有诸多好处

  • 类型声明比PropTypes声明要便捷的多
  • 约束propsstate类型和操作
  • 事件处理类型检查

> 尽管React提供了prop-types库可以为组件添加运行时参数检查能力,但是需要在运行后才能发现问题,体验和查错能力上远逊于静态类型检查。

使用

声明组件

过去,官方建议是使用prop-types加入检查。

1
2
3
4
5
6
7
8
9
10
11
12
13
import React from 'react';
import PropTypes from 'prop-types';

class MyComponent extends React.Component {
static propTypes = {
foo: PropTypes.number.isRequired,
bar: PropTypes.string,
};

render() {
return <div>{this.props.xxx}</div>;
}
}

在编写代码时,直到代码运行之前,我们并不知道会不会出错。

下面是在引入flow后的写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// @flow

import * as React from 'react';

type Props = {
foo: number,
bar?: string, // 可选的
};

class MyComponent extends React.Component<Props> {
render() {
return <div>{this.props.xxx}</div>;// Error! 'a' is missing in Props
}
}

<MyComponent foo="1" /> Error! 'foo' is 'number' but passed 'string'

可以看到,不仅在代码上简练了很多,可读性也更强,并且在编写过程中,flow已经告诉了我们错误。

如果想限定传入的props,防止父组件传入额外的无用属性,只需要将Props对象类型声明成完全匹配类型即可。如在某些情况使用将...赋值在一个img节点上,为了避免传入了未知的img节点属性(react运行时会抛错),可以限制此组件传入参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
// @flow

type Props = {|
src: string,
|};

class MyImage extends React.Component<Props, State> {
render(){
return <img {...this.props} />
}
}

<MyImage src="https://..." foo={1}> // Error! 'foo' is not defined

同样的,下面对state加入检查。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// @flow

import * as React from 'react';

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

type State = {
a: number
};

class MyComponent extends React.Component<Props, State> {
state = {
a: 10
}

componentDidMount(){
setInterval(()=>{
this.setState({
a: this.state.a + 1,
b: 2, // Error! 'b' is missing in State
});
}, 1000);
}

render() {
return <div>{this.state.a}</div>;
}
}

在组件中任何地方访问未声明的state属性,或者传入了错误的state字段,flow都会抛出错误。

除了类组件,React还有函数组件,函数组件没有state,声明就更简单了。

1
2
3
4
5
6
7
8
9
10
11
// @flow

import * as React from 'react';

type Props = {
foo: number,
};

function MyComponent(props: Props) {
// xxx
}

默认属性

React支持对props设置默认属性。如果某个字段有默认属性,它就自动变成可选字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// @flow

import * as React from 'react';

type Props = {
foo: number,
bar: string, // 和`bar?: string`一样是可选的
};

class MyComponent extends React.Component<Props> {
static defaultProps = {
bar: '',
}

render() {
return <div>{this.props.foo}</div>;
}
}

<MyComponent foo={1} /> // OK

对函数组件也是如此

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// @flow

import * as React from 'react';

type Props = {
foo: number,
bar: string, // 和`bar?: string`一样是可选的
};

function MyComponent(props:Props) {
return <div>{this.props.foo}</div>;
}

MyComponent.defaultProps = {
bar: '',
}

// ...
<MyComponent foo={1} /> // OK

// ...

事件处理

在flow中推荐使用箭头函数属性的方式定义事件回调函数,这也是React中定义事件回调最方便的方式。

在回调函数参数中对参数event加入SyntheticEvent<T>声明。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import * as React from 'react';

class MyComponent extends React.Component<{}, { count: number }> {
handleClick = (event: SyntheticEvent<HTMLButtonElement>) => {
(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>
);
}
}

如果想要对事件的详细类型,flow提供了下面几种事件类型。

  • SyntheticEvent<T> for Event
  • SyntheticAnimationEvent<T> for AnimationEvent
  • SyntheticCompositionEvent<T> for CompositionEvent
  • SyntheticInputEvent<T> for InputEvent
  • SyntheticUIEvent<T> for UIEvent
  • SyntheticFocusEvent<T> for FocusEvent
  • SyntheticKeyboardEvent<T> for KeyboardEvent
  • SyntheticMouseEvent<T> for MouseEvent
  • SyntheticDragEvent<T> for DragEvent
  • SyntheticWheelEvent<T> for WheelEvent
  • SyntheticTouchEvent<T> for TouchEvent
  • SyntheticTransitionEvent<T> for TransitionEvent

ref

首先要吐槽一下

官方文档居然没更新V16的新的ref语法声明方式!!

老版的回调函数:

1
2
3
4
5
6
7
8
9
10
import * as React from 'react';

class MyComponent extends React.Component<{}> {
// button要设置成可能类型,因为可能并不存在
button: ?HTMLButtonElement;

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

那么新版的呢,只有自己动手了。稍微研究一下会发现,React.createRef其实是一个泛型方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import * as React from 'react';

class MyComponent extends React.Component<{}> {

ref = React.createRef<'button'>();

componentDidMount() {
this.ref.current; // Error! 'current'可能为null
if (this.ref.current) {
this.ref.current.id; // OK!
this.ref.current.src; // Error! 'src'并不是'button'标签的属性
}
}

// button要设置成可能类型,因为可能并不存在
button: ?HTMLButtonElement;

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

Children

总所周知,React的children可以为很多种数据类型,一个个声明的话绝对让人崩溃,还好flow提供了一个简单的方式。

1
2
3
4
5
import * as React from 'react';

type Props = {
children: React.Node
}

你可能注意到了上面使用了import * as React from 'react';引入React,而不是import React from 'react。通过查看flow源码可以看到,flow内部已经声明了React的所有类型,使用import * as的方式可以将类型也一起导出。

React.Node声明:

1
2
3
4
5
6
7
8
declare type React$Node =
| null
| boolean
| number
| string
| React$Element<any>
| React$Portal
| Iterable<?React$Node>;
特定的Children元素

有时候我们需要组件的children做限制,比如接受一个函数组件,接受一个特定组件实例,或者多个特定组件。

函数组件:

1
2
3
4
5
// @flow

type Props = {
children: ({a: number, b: number}) => React.Node
}

特定组件实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// @flow

import * as React from 'react';

type Props = {
children: React.Element<'div'>
}

class X extends React.Component<Props> {
render() {
return this.props.children;
}
}

<X><div/></X> // OK
<X><img /></X> // Error

特定的自定义组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// @flow

import * as React from 'react';

class A extends React.Component<{}> {
render() {
return <div>a</div>;
}
}

type Props = {
children: React.Element<typeof A>
}

class X extends React.Component<Props> {
render() {
return this.props.children;
}
}

<X><A /></X> // OK
<X><img /></X> // Error

当然children可能为多个组件,比如选项卡组件,这时需要使用React.ChildrenArray

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// @flow

import * as React from 'react';

class TabItem extends React.Component<{}> {
render() {
return <div>a</div>;
}
}

type Props = {
children: React.ChildrenArray<React.Element<typeof TabItem>>
}

class Tab extends React.Component<Props> {
render() {
return this.props.children;
}
}

(
<Tab>
<TabItem />
<TabItem />
</Tab>
) // OK

children同样可能为函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// @flow

import * as React from 'react';

type Props = {
children: () => React.Node
}

class X extends React.Component<Props> {
render() {
return this.props.children();
}
}

<X>
{() => <div>foo</div>}
</X> // OK

高阶组件

高阶组件(High Order Component)是react中常用的一种模式,它一般来说定义一个函数,接受一个组件以及额外参数,并返回一个新的组件。例如react-reduxconnectreact-router中的withRouter都是高阶组件。

高阶组件的作用在于创建可复用的逻辑,并将其通过props传给传入的组件。也就是说在调用高阶组件时,可能需要传入额外的props或者更少的props(一般来说是更少)。引入flow声明也是如此。

例如,高阶组件会自动传入foo

1
2
3
4
5
6
7
8
9
// @flow

import * as React from 'react';

function injectFoo<Props:{}>(Comp: React.ComponentType<Props>): React.ComponentType<$Diff<Props, {foo?: number}>>{
return function(props: Props){
return <Component {...props} foo={42} />;
}
}

foo需要声明成可选类型,不然$Diff会报错。$Diff<{ foo: number }, { foo?: number }>等价于{ foo?: number }

上面我们使用了泛型Props作为了参数,但是上面提到过定义了defaultProps的参数也是可选的,这样一来Props就不是完全准确的定义了组件所需的类型。好在Flow提供了React.ElementConfig帮助我们获取准确的Props定义。

1
2
3
4
5
6
// @flow
function myHOC<Props, Component: React.ComponentType<Props>>(
WrappedComponent: Component
): React.ComponentType<React.ElementConfig<Component>> {
return props => <WrappedComponent {...props} />;
}

我们定义了另一个泛型参数Component,再通过React.ElementConfig<Component>代替了Props作为组件的参数。

Context

从React v16.3起新增了React.createContext api,用于替换以前复杂的context使用方式。

React.createContext本身是个泛型方法,传入的泛型参数作为value的类型,如下:

1
2
3
4
5
import React from "react";

const Theme = React.createContext<"light" | "dark">("light");

<Theme.Provider value="blue" />; // Error! "blue" is not one of the allowed values.

React工具类

React.Node

等价于

1
2
// @flow
React.ChildrenArray<void | null | boolean | string | number | React.Element<any>>;
React.Element<typeof Component>

声明一个组件的实例元素,也是React.createElement()方法的返回类型。

1
2
3
4
5
// @flow

(<div />: React.Element<'div'>); // OK
(<span />: React.Element<'span'>); // OK
(<div />: React.Element<'span'>); // Error: div is not a span.
1
2
3
4
5
6
7
8
// @flow

class Foo extends React.Component<{}> {}
function Bar(props: {}) {}

(<Foo />: React.Element<typeof Foo>); // OK
(<Bar />: React.Element<typeof Bar>); // OK
(<Foo />: React.Element<typeof Bar>); // Error: Foo is not Bar
React.ChildrenArray<T>

定义特定类型的children,见上文。

React.ComponentType<Props>

定义特定Props的组件,包括函数组件和类组件。

React.StatelessFunctionalComponent<Props>

同上,不过特定无状态的函数组件。

React.ElementType

React.ComponentType<Props>类似,不同之处在于它支持jsx的内联元素,如div

它的声明大致为

1
2
3
declare type React$ElementType =
| string
| React.ComponentType<any>
React.Key

用于声明react的key值类型

1
type Key = string | number;
React.Ref<typeof Component>

用于声明Ref,支持V16的新的api与旧的语法

1
2
3
4
type React$Ref<ElementType: React$ElementType> =
| {current: React$ElementRef<ElementType> | null}
| ((React$ElementRef<ElementType> | null) => mixed)
| string;
React.ElementProps<typeof Component>

用于获取一个组件的的Props类型。

React.ElementConfig<typeof Component>

同上,不同之处在于会处理defaultProps属性为可选参数。

React.ElementRef<typeof Component>

用于获取一个组件的实例类型。

  • 类组件会返回类的实例

  • 函数组件会返回undefined

  • jsx内联属性会返回DOM实例,例如React.ElementRef<'div'>会返回HTMLDivElement

注意和React.Element<typeof Component>不同。一个返回React元素,一个返回组件的实例。

参考资料

Flow+React