React 组件的演化
组件复用方式 | 优势 | 劣势 | 状态 |
---|---|---|---|
类组件(Class ) |
发展时间长,接受度广泛 | 只能继承父类 | 作为一种传统开发模式,会长期存在 |
Mixin |
可以复制任意对象的任意多个方法,实现组件间的复用 | 组件间相互依赖、耦合,可能产生冲突,不利于维护 | 被官方抛弃 |
高阶组件(HOC) | 利用装饰器模式,在不改变组件的基础上,动态地为其添加新的能力 | 嵌套过多调试困难,需要遵循某些约定(不改变原始组件,透传 props 等) |
能力强大,广泛引用 |
Hooks |
替代类组件,多个 Hooks 间互不影响,避免嵌套地狱,开发效率高 |
切换新思维需要成本 | React 的未来(官方主推) |
函数组件
普通函数组件
上一篇中,我们创建了一个无状态组件(没有状态影响,作为纯静态展示) <Hello />
,同时它也是一个函数组件。
import React from "react";
import { Button } from "antd";
interface Greeting {
name: string;
firstName: string;
lastName: string;
}
const Hello = (props: Greeting) => <Button>hello {props.name}</Button>;
Hello.defaultProps = {
firstName: "",
lastName: "",
};
export default Hello;
复制代码
React.FC
在 react 的声明文件中,对函数组件单独定义了一个类型 — React.FC
:
type React.FC<P = {}> = React.FunctionComponent<P>
复制代码
现在使用 React.FC
重新定义一下 <Hello />
:
const Hello: React.FC<Greeting> = ({ name, firstName, lastName }) => (
<Button>hello {name}</Button>
);
复制代码
使用 React.FC 后的区别
FC
是 FunctionComponent
的简写,这个类型定义了默认的 props
(如 children
)。
const Hello: React.FC<Greeting> = ({ name, firstName, lastName, children }) => (
<Button>hello {name}</Button>
);
复制代码
在使用 React.FC
后定义 defaultProps
时,默认属性必须是可选的(这和普通函数组件不同):
interface Greeting {
name: string;
firstName?: string;
lastName?: string;
}
const Hello: React.FC<Greeting> = ({ name, firstName, lastName, children }) => (
<div>
<Button>hello {name}</Button>
</div>
);
Hello.defaultProps = {
firstName: "",
lastName: "",
};
复制代码
小结
在 TypeScript
中,函数组件需要为 props
定义类型。
类组件
Component
类组件需要继承 Components
组件,在 react
的声明文件中,Component
被定义为泛型类:
// P: 属性的类型,默认{}
// S: 状态的类型,默认{}
// SS: snapshot
(alias) class Component<P = {}, S = {}, SS = any>
复制代码
实现
将上面的函数组件改造成类组件:
// src/componets/demo/HellpClass.tsx
import React, { Component } from "react";
import { Button } from "antd";
interface Greeting {
name: string;
firstName: string;
lastName: string;
}
interface State {
count: number;
}
class HelloClass extends Component<Greeting, State> {
// 初始化 state
state: State = { count: 0 };
// 默认属性值
static defaultProps = {
firstName: "",
lastName: "",
};
render() {
return <Button>hello {this.props.name}</Button>;
}
}
export default HelloClass;
复制代码
通过 setState
为 <HelloClass />
添加一个点击计数功能。
class HelloClass extends Component<Greeting, State> {
state: State = { count: 0 };
static defaultProps = {
firstName: "",
lastName: "",
};
render() {
return (
<>
<div>您点击了 {this.state.count} 次</div>
<Button
onClick={() => {
this.setState({ count: this.state.count + 1 });
}}
>
hello {this.props.name}
</Button>
</>
);
}
}
复制代码
小结
在 TypeScript
中,类组件需要为 props
和 state
定义类型。
高阶组件
我们现在要利用高阶组件包装一下 <HelloClass />
,包装后的组件有一个新属性 loading
,通过该属性控制被包装组件的 显示/隐藏。
React.ComponentType
指定被包装组件的类型为 React.ComponentType
(一种 React
预定义类型),既可以是类组件,也可以是函数组件:
type React.ComponentType<P = {}> = React.ComponentClass<P, any> | React.FunctionComponent<P>
复制代码
实现
添加 <HelloHOC />
组件:
import React, { Component } from "react";
import HelloClass from "./HelloClass";
interface Loading {
loading: boolean;
}
/*
** WrapperComponetn: 需要被包装的组件
*/
function HelloHOC<P>(WrapperComponetn: React.ComponentType<P>) {
// 定义 props 为 P 和 Loading 的交叉类型
return class extends Component<P & Loading> {
render() {
// 解构 props,拆分出 loading
const { loading, ...props } = this.props;
// {...props}:属性透传
return loading ? (
<div>Loading...</div>
) : (
<WrapperComponetn {...(props as P)} />
);
}
};
}
// 导出经过高阶组件包装后的组件
export default HelloHOC(HelloClass);
复制代码
有个报错
我们在 index.tsx
中引入这个组件,这时会有一个报错:
import React from "react";
import ReactDOM from "react-dom";
import HelloHOC from "./components/demo/HelloHOC";
ReactDOM.render(
<HelloHOC name="typescript" loading={true} />,
document.querySelectorAll(".app")[0]
);
// ERROR! 因为 HelloClass 的静态属性 defaultProps 传不出来。
复制代码
解决方案:将 defaultProps
设置为可选属性。
interface Greeting {
name: string;
firstName?: string;
lastName?: string;
}
复制代码
小结
在 TypeScript
中,高阶组件的使用会遇到很多类型问题,还有可能遇到一些已知的 bug
,但这并不是高阶组件本身的问题,而是因为 react
声明文件没有很好的兼容。其实官方最推荐的是使用 Hooks
,下面就再用 Hooks
实现一下吧
hooks
hooks
也是一种函数组件,对比类组件,明显简化了许多:
import React, { useEffect, useState } from "react";
import { Button } from "antd";
interface Greeting {
name: string;
firstName: string;
lastName: string;
}
const HelloHooks = (props: Greeting) => {
// 定义 [组件的状态,设置状态的方法],给定状态的初始值:不需要再定义类型
const [count, setCount] = useState(0);
const [text, setText] = useState<string | null>(null);
return (
<>
<div>您点击了 {count} 次</div>
<Button
onClick={() => {
setCount(count + 1);
}}
>
hello {props.name}
</Button>
</>
);
};
HelloHooks.defaultProps = {
firstName: "",
lastName: "",
};
export default HelloHooks;
复制代码
利用 useEffect
新增一个功能: 点击超过 5 次给出提示。
const HelloHooks = (props: Greeting) => {
const [count, setCount] = useState(0);
const [text, setText] = useState<string | null>(null);
// 只有当 count 改变时,渲染逻辑才会执行。
useEffect(() => {
if (count > 5) {
setText("休息一下");
}
}, [count]);
return (
<>
<div>
您点击了 {count} 次,{text}
</div>
<Button
onClick={() => {
setCount(count + 1);
}}
>
hello {props.name}
</Button>
</>
);
};
复制代码
为什么要定义为泛型?
不用泛型变量,this.props
的类型无法确定,在内部只能使用类型断言来访问属性:
(this.props as Greeting).name;
复制代码
这样很麻烦,而 React
声明文件把这些约束关系都用泛型定义好了。