组件声明

在React中,组件的声明方式有两种:函数组件类组件, 来看看这两种类型的组件声明时是如何定义TS类型的。

类组件

类组件的定义形式有两种:React.Component<P, S={}>React.PureComponent<P, S={} SS={}>,它们都是泛型接口,接收两个参数,第一个是props类型的定义,第二个是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
interface IProps {
name: string;
}

interface IState {
count: number;
}

class App extends React.Component<IProps, IState> {
state = {
count: 0
};

render() {
return (
<div>
{this.state.count}
{this.props.name}
</div>
);
}
}

export default App;

React.PureComponent<P, S={} SS={}> 也是差不多的:

1
class App extends React.PureComponent<IProps, IState> {}

React.PureComponent是有第三个参数的,它表示getSnapshotBeforeUpdate的返回值。

那PureComponent和Component 的区别是什么呢?它们的主要区别是PureComponent中的shouldComponentUpdate 是由自身进行处理的,不需要我们自己处理,所以PureComponent可以在一定程度上提升性能。

那如果定义时候我们不知道组件的props的类型,只有在调用时才知道组件类型,该怎么办呢?这时泛型就发挥作用了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 定义组件
class MyComponent<P> extends React.Component<P> {
internalProp: P;
constructor(props: P) {
super(props);
this.internalProp = props;
}
render() {
return (
<span>hello world</span>
);
}
}

// 使用组件
type IProps = { name: string; age: number; };

<MyComponent<IProps> name="React" age={18} />; // Success
<MyComponent<IProps> name="TypeScript" age="hello" />; // Error

函数组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface IProps {
name: string
}

const App = (props: IProps) => {
const {name} = props;

return (
<div className="App">
<h1>hello world</h1>
<h2>{name}</h2>
</div>
);
}

export default App;

除此之外,函数类型还可以使用React.FunctionComponent<P={}>来定义,也可以使用其简写React.FC<P={}>,两者效果是一样的。它是一个泛型接口,可以接收一个参数,参数表示props的类型,这个参数不是必须的。它们就相当于这样:

1
type React.FC<P = {}> = React.FunctionComponent<P>

最终的定义形式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface IProps {
name: string
}

const App: React.FC<IProps> = (props) => {
const {name} = props;
return (
<div className="App">
<h1>hello world</h1>
<h2>{name}</h2>
</div>
);
}

export default App;

当使用这种形式来定义函数组件时,props中默认会带有children属性,它表示该组件在调用时,其内部的元素,来看一个例子,首先定义一个组件,组件中引入了Child1和Child2组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import Child1 from "./child1";
import Child2 from "./child2";

interface IProps {
name: string;
}
const App: React.FC<IProps> = (props) => {
const { name } = props;
return (
<Child1 name={name}>
<Child2 name={name} />
TypeScript
</Child1>
);
};

export default App;

Child1组件结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface IProps {
name: string;
}
const Child1: React.FC<IProps> = (props) => {
const { name, children } = props;
console.log(children);
return (
<div className="App">
<h1>hello child1</h1>
<h2>{name}</h2>
</div>
);
};

export default Child1;

我们在Child1组件中打印了children属性,它的值是一个数组,包含Child2对象和后面的文本:

image-20211027145219797

使用 React.FC 声明函数组件和普通声明的区别如下:

  • React.FC 显式地定义了返回类型,其他方式是隐式推导的;
  • React.FC 对静态属性:displayName、propTypes、defaultProps 提供了类型检查和自动补全;
  • React.FC 为 children 提供了隐式的类型(ReactElement | null)。

那如果我们在定义组件时不知道props的类型,只有调用时才知道,那就还是用泛型来定义props的类型。对于使用function定义的函数组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 定义组件
function MyComponent<P>(props: P) {
return (
<span>
{props}
</span>
);
}

// 使用组件
type IProps = { name: string; age: number; };

<MyComponent<IProps> name="React" age={18} />; // Success
<MyComponent<IProps> name="TypeScript" age="hello" />; // Error

React Hooks

useState

默认情况下,React会为根据设置的state的初始值来自动推导state以及更新函数的类型:

image-20211027152037738

如果已知state 的类型,可以通过以下形式来自定义state的类型:

1
const [count, setCount] = useState<number>(1)

如果初始值为null,需要显式地声明 state 的类型:

1
const [count, setCount] = useState<number | null>(null); 

如果state是一个对象,想要初始化一个空对象,可以使用断言来处理:

1
const [user, setUser] = React.useState<IUser>({} as IUser);

实际上,这里将空对象{}断言为IUser接口就是欺骗了TypeScript的编译器,由于后面的代码可能会依赖这个对象,所以应该在使用前及时初始化 user 的值,否则就会报错。

根据useState在类型声明文件中的定义:定义两种形式,分别是有初始值和没有初始值的形式。

useEffect

useEffect的主要作用就是处理副作用,它的第一个参数是一个函数,表示要清除副作用的操作,第二个参数是一组值,当这组值改变时,第一个参数的函数才会执行,这让我们可以控制何时运行函数来处理副作用:

1
2
3
4
5
6
7
8
9
10
useEffect(
() => {
const subscription = props.source.subscribe();
return () => {
subscription.unsubscribe();
};
},
[props.source]
);

当函数的返回值不是函数或者effect函数中未定义的内容时,如下:

1
2
3
4
5
6
7
useEffect(
() => {
subscribe();
return null;
}
);

TypeScript就会报错:

image-20211027152509367

根据useEffect在类型声明文件中的定义:useEffect的第一个参数只允许返回一个函数。

useContext

useContext需要提供一个上下文对象,并返回所提供的上下文的值,当提供者更新上下文对象时,引用这些上下文对象的组件就会重新渲染:

1
2
3
4
5
6
7
const ColorContext = React.createContext({ color: "green" });

const Welcome = () => {
const { color } = useContext(ColorContext);
return <div style={{ color }}>hello world</div>;
};

在使用useContext时,会自动推断出提供的上下文对象的类型,所以并不需要我们手动设置context的类型。当前,我们也可以使用泛型来设置context的类型:

1
2
3
4
5
6
interface IColor {
color: string;
}

const ColorContext = React.createContext<IColor>({ color: "green" });

其他

import React

在React项目中使用TypeScript时,普通组件文件后缀为.tsx,公共方法文件后缀为.ts。在. tsx 文件中导入 React 的方式如下:

1
2
3
import * as React from 'react'
import * as ReactDOM from 'react-dom'

这是一种面向未来的导入方式,如果想在项目中使用以下导入方式:

1
2
3
import React from "react";
import ReactDOM from "react-dom";

就需要在tsconfig.json配置文件中进行如下配置:

1
2
3
4
5
"compilerOptions": {
// 允许默认从没有默认导出的模块导入。
"allowSyntheticDefaultImports": true,
}

Types or Interfaces?

我们可以使用types或者Interfaces来定义类型吗,那么该如何选择他俩呢?建议如下:

  • 在定义公共 API 时(比如编辑一个库)使用 interface,这样可以方便使用者继承接口,这样允许使用最通过声明合并来扩展它们;
  • 在定义组件属性(Props)和状态(State)时,建议使用 type,因为 type 的约束性更强。

interface 和 type 在 ts 中是两个不同的概念,但在 React 大部分使用的 case 中,interface 和 type 可以达到相同的功能效果,type 和 interface 最大的区别是:type 类型不能二次编辑,而 interface 可以随时扩展:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
interface Animal {
name: string
}

// 可以继续在原属性基础上,添加新属性:color
interface Animal {
color: string
}

type Animal = {
name: string
}
// type类型不支持属性扩展
// Error: Duplicate identifier 'Animal'
type Animal = {
color: string
}

type对于联合类型是很有用的,比如:type Type = TypeA | TypeB。而interface更适合声明字典类行,然后定义或者扩展它。