likes
comments
collection
share

React Hook + Typescript 实现一个类型提示完整的高阶组件(HOC)

作者站长头像
站长
· 阅读数 17

我报名参加金石计划一期挑战——瓜分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 帮助用户填写表单。

React Hook + Typescript 实现一个类型提示完整的高阶组件(HOC)

如果触发了表单验证,还需要再增加一个错误提示。

React Hook + Typescript 实现一个类型提示完整的高阶组件(HOC)

而除了这些每个表单项都有的东西,中间的表单组件却不尽相同,可能是输入框、选择器、单选框、多选框等等组件,因此需要有一定的拓展性。那么这时候我们就可以通过高阶组件实现将通用功能的抽离,并且能动态传入组件,任意进行拓展。

高阶组件的实现

基础框架

接下来我们尝试按照上面的分析实现一个高阶组件,高阶组件一般以 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}
/>

代码提示非常友好,而且根据你选择的组件不同,参数类型也会根据你所传入高阶组件的范型进行提示,避免参数误传或少传: React Hook + Typescript 实现一个类型提示完整的高阶组件(HOC)

React Hook + Typescript 实现一个类型提示完整的高阶组件(HOC)

注意点

在返回的组件中,我并没有直接将参数一个一个传入 Component 中,而是使用以下写法,

const componentProps = { ...props } as FieldType;
<Component {...componentProps} />

如果你直接将参数传入组件会得到这个报错:

<Component name={name} />

React Hook + Typescript 实现一个类型提示完整的高阶组件(HOC)

总结

这篇文章为大家介绍了高阶组件的概念及使用场景,以及如何配置高阶组件的类型,当然实现的方式与业务有部分关联,如果你有更好的实现方式不如在评论区指出。希望这篇文章可以为大家解决一些问题,如果觉得文章对你有帮助不妨点个赞👍