React Hook + Typescript 实现一个类型提示完整的高阶组件(HOC)
我报名参加金石计划一期挑战——瓜分10万奖池,这是我的第1篇文章,点击查看活动详情
前言
最近在对项目内的表单组件进行的优化,在实现高阶组件的过程中发现目前关于 react 高阶组件的文章都比较旧了,很多都是以类组件的形式去实现,与 Typescript 的结合使用也比较难找到示例。因此决定自己写一篇关于 Recat hook + Typescript 实现高阶组件的文章,希望简单直观的让大家学习到高阶组件的使用场景及封装方式。
什么是高阶组件
高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。
高阶组件其实就是一个函数,这个 函数的参数是一个组件,返回值也是一个组件。功能也就是将传入的组件进行一些通用的处理,再将处理过的组件返回。举个例子:
const WithHelloWorld = (Component) => {
const extraProps = 'hello world';
const newComponent = (props) => {
console.log(WithHelloWorld);
return <Component extraProps={extraProps} {...props} />;
};
return newComponent;
};
上面这段代码实现了一个 WithHelloWorld
的高阶组件,功能就是为传入的组件添加一个 extraProps
参数,并且在使用时会自动打印 hello world
。
高阶组件的作用
高阶组件的作用其实就是将一些通用的逻辑进行抽离,在实现新的组件时不需要重复实现已有逻辑。下面将将项目中使用的案例。
例如在一个表单中,如果每一个表单项都会有上面的 formLabel 作为字段名,下面的 helperText 帮助用户填写表单。
如果触发了表单验证,还需要再增加一个错误提示。
而除了这些每个表单项都有的东西,中间的表单组件却不尽相同,可能是输入框、选择器、单选框、多选框等等组件,因此需要有一定的拓展性。那么这时候我们就可以通过高阶组件实现将通用功能的抽离,并且能动态传入组件,任意进行拓展。
高阶组件的实现
基础框架
接下来我们尝试按照上面的分析实现一个高阶组件,高阶组件一般以 With + xxx
格式进行命名,这里我们将组件命名为 WithFormItem
。以下的代码都是通过 ChakraUI + react hook form
进行实现的。
首先实现一个最小的高阶组件框架:
// WithFormItem.tsx
const WithFormItem = (Component) => {
const FormItem = (props) => {
return <Component {...props}></Component>
}
return FormItem
}
export default WithFormItem
结合 TS
解下来要考虑一下如何实现结合 ts 实现类型提示,这一步还是有些坑的,首先我们需要知道我们在调用高阶组件的时候,其实调用的是里面返回的 FormItem
组件,因此我们需要将类型从 WithFormItem
函数传到 FormItem
内的,这里可以使用泛型实现:
export type BaseFieldProps = {
name: string;
error: boolean;
label?: string;
toolTip?: JSX.Element | string;
};
export type DefaultProps = {
name: string;
label?: string;
unit?: string;
isOptional?: boolean;
helperText?: string | React.ReactNode;
labelTooltip?: string;
isRequired?: boolean;
max?: number;
min?: number;
maxLength?: number;
minLength?: number;
};
const WithFormItem = <FieldType extends BaseFieldProps>(
Component: React.FC<FieldType>
) => {
const FormItem: React.FC<
DefaultProps & Omit<FieldType, keyof BaseFieldProps>
> = (props) => {
const componentProps = { ...props } as FieldType;
return <Component {...componentProps} />;
};
return FormItem;
};
export default WithFormItem;
咱们分析一下上面的代码,首先我们为 WithFormItem 函数定义了一个泛型 FieldType
,这个泛型代表的是使用高阶组件函数时,我们需要传入的 待处理组件 的参数类型,FieldType
继承了 BaseFieldProps
,说明待处理的组件上至少得具有 BaseFieldProps
定义的这几个参数,否则是会提示的。
而 处理后的组件 的类型则比较复杂,DefaultProps
说明处理后的组件上至少得具有 DefaultProps
定义的这几个参数 ,而 DefaultProps & Omit<FieldType, keyof BaseFieldProps>
这一步操作则是将
将前面传入的泛型 FieldType
中的 BaseFieldProps
取差集,再与 DefaultProps
取并集,最终返回的组件类型为 DefaultProps + FieldType - BaseFieldProps
。
举个使用的例子:
import WithFormItem, { BaseFieldProps } from './WithFormItem.tsx';
export type TextareaProps = BaseFieldProps & {customProp: "自定义参数" };
const Textarea: React.FC<TextareaProps> = ({ name, field, error, ...rest }) => (
<ChakraTextarea w="100%" isInvalid={error} {...field} {...rest} />
);
export WithFormItem<TextareaProps>(Textarea)
当我们使用 Textarea
组件时,我们自定义的参数只有一个 customProp
, 传入高阶组件就会替我们添加上 DefaultProps
中的所有参数,在使用组件时就可以获得完整的类型提示了。
组件使用
在上面的代码中,我并没有展示具体的逻辑实现过程代码,大家在类型理解后自行根据业务实现即可,下面讲讲我是怎么使用高阶组件的:
import React, { memo } from 'react';
const WithFormItem = <FieldType extends BaseFieldProps>(
Component: React.FC<FieldType>
) => {
const FormItem: React.FC<
DefaultProps & Omit<FieldType, keyof BaseFieldProps>
> = (props) => {
const componentProps = { ...props } as FieldType;
return <Component {...componentProps} />;
};
return memo(FormItem);
};
在返回处理后的组件时使用 memo
进行包裹,React.momo
其实也是一个高阶组件噢。
// FormItem/index.tsx
import WithFormItem from './WithFormItem';
import TextInput, { TextInputProps } from './TextInput';
import NumberInput, { NumberInputProps } from './NumberInput';
import Textarea, { TextareaProps } from './Textarea';
import Password, { PasswordProps } from './Password';
import Checkbox, { CheckboxProps } from './Checkbox';
import Select, { SelectProps } from './Select';
import MultiSelect, { MultiSelectProps } from './MultiSelect';
import LabelCreate, { LabelCreateProps } from './LabelCreate';
import CodeEditor, { CodeEditorProps } from './CodeEditor';
const FormItem = {
Text: WithFormItem<TextInputProps>(TextInput),
Number: WithFormItem<NumberInputProps>(NumberInput),
Textarea: WithFormItem<TextareaProps>(Textarea),
Checkbox: WithFormItem<CheckboxProps>(Checkbox),
Select: WithFormItem<SelectProps>(Select),
Password: WithFormItem<PasswordProps>(Password),
MultiSelect: WithFormItem<MultiSelectProps>(MultiSelect),
LabelCreate: WithFormItem<LabelCreateProps>(LabelCreate),
CodeEditor: WithFormItem<CodeEditorProps>(CodeEditor),
};
export default FormItem;
然后将所有的表单组件在 index.tsx
中统一引入进行处理,再进行暴露一个 FormItem
对象,对象上的每一个属性对应一个组件。
在页面中引用的时候,就非常方便了,直接通过 .
语法获取组件,使用方式如下:
import FormItem from "components"
<FormItem.Text
name="name"
label="Name"
isRequired
maxLength={128}
/>
代码提示非常友好,而且根据你选择的组件不同,参数类型也会根据你所传入高阶组件的范型进行提示,避免参数误传或少传:
注意点
在返回的组件中,我并没有直接将参数一个一个传入 Component
中,而是使用以下写法,
const componentProps = { ...props } as FieldType;
<Component {...componentProps} />
如果你直接将参数传入组件会得到这个报错:
<Component name={name} />
总结
这篇文章为大家介绍了高阶组件的概念及使用场景,以及如何配置高阶组件的类型,当然实现的方式与业务有部分关联,如果你有更好的实现方式不如在评论区指出。希望这篇文章可以为大家解决一些问题,如果觉得文章对你有帮助不妨点个赞👍
转载自:https://juejin.cn/post/7139157115666432037