前情提要
在浏览掘金的时候,我看到了一篇关于实现类型提示完整的高阶组件的文章 , React Hook + Typescript 实现一个类型提示完整的高阶组件(HOC)。于是我便想把项目中的 Form
组件也用高阶组件封装一下。然而,在封装过程中,我发现了一个 antd
组件的 bug
。但是值得称赞的是,提了 issue
以后,不到半小时就有人提供了 pr
修复了这个问题。。不得不说,antd
社区的响应速度值得称赞
封装过程
以 input
组件为例子,我想达到的效果如下
import { FormInput } from 'components/Form'
<FormInput
label="姓名"
name="name"
rules={[{ required: true, message: '请输入姓名'}]}
bordered={false}
/>
作为一个基于 typescript
的高阶组件,类型提示是必不可少的。antd
组件给我们提供了像 FormItemProps
, InputProps
、SelectProps
等等的类型。因此,我们可以直接使用这些类型来给我们的组件定义类型
下面是核心的高阶组件代码:
const WithFormItem = <T,>(Component: React.ComponentType<T>) => {
const FormItem = (props: FormItemProps & T) => {
const {
colon,
dependencies,
extra,
getValueFromEvent,
getValueProps,
hasFeedback,
help,
hidden,
htmlFor,
label,
labelAlign,
labelCol,
messageVariables,
name,
normalize,
noStyle,
preserve,
required,
rules,
shouldUpdate,
tooltip,
trigger,
validateFirst,
validateStatus,
validateTrigger,
valuePropName,
wrapperCol,
...otherProps
} = props;
const formItemProps: FormItemProps = {
colon,
dependencies,
extra,
getValueFromEvent,
getValueProps,
hasFeedback,
help,
hidden,
htmlFor,
label,
labelAlign,
labelCol,
messageVariables,
name,
normalize,
noStyle,
preserve,
required,
rules,
shouldUpdate,
tooltip,
trigger,
validateFirst,
validateStatus,
validateTrigger,
valuePropName,
wrapperCol,
};
return (
<Form.Item {...formItemProps}>
<Component {...(otherProps as T & FormItemProps)} />
</Form.Item>
);
};
return React.memo(FormItem);
};
主要问题就是区分 FormItemProps
和其他 Component
的 props
,搜索了半天也没有特别好的解决方法 , 最后决定直接穷举出 FormItemProps
中的所有属性,然后在将剩余的属性传递给 Component
,这样就可以实现类型提示了。
最后我们使用这个高阶组件来简单封装一下一些自己项目的通用代码
import {
FormItemProps,
Input,
Form,
InputProps,
Select,
SelectProps,
DatePickerProps,
DatePicker,
InputNumber,
InputNumberProps,
} from 'antd';
import { RangePickerProps } from 'antd/es/date-picker';
import { TextAreaProps } from 'antd/es/input';
export const FormItem = {
Input: WithFormItem<InputProps>(Input),
Select: WithFormItem<SelectProps>(Select),
DatePicker: WithFormItem<DatePickerProps>(DatePicker),
TextArea: WithFormItem<TextAreaProps>(Input.TextArea),
InputNumber: WithFormItem<InputNumberProps>(InputNumber),
RangePicker: WithFormItem<RangePickerProps>(DatePicker.RangePicker),
};
export const FormInput = (props: FormItemProps & InputProps) => {
return <FormItem.Input allowClear placeholder={`请输入${props.label}`} {...props} />;
};
export const FormSelect = (props: FormItemProps & SelectProps) => {
return (
<FormItem.Select
allowClear
placeholder={`请选择${props.label}`}
getPopupContainer={(triggerNode) => triggerNode.parentNode}
{...props}
/>
);
};
export const FormDatePicker = (props: FormItemProps & DatePickerProps) => {
return <FormItem.DatePicker allowClear placeholder={`请选择${props.label}`} {...props} />;
};
export const FormRangePicker = (props: FormItemProps & RangePickerProps) => {
return <FormItem.RangePicker allowClear allowEmpty={[true, true]} {...props} />;
};
export const FormInputNumber = (props: FormItemProps & InputNumberProps) => {
return <FormItem.InputNumber placeholder={`请输入${props?.label}`} {...props} />;
};
export const FormTextArea = (props: FormItemProps & TextAreaProps) => {
return <FormItem.TextArea allowClear rows={4} placeholder={`请输入${props?.label}`} {...props} />;
};
bug 的出现
就在我美滋滋的准备在项目中使用这个封装好的组件的时候,我发现了一个 bug
。bug
的复现特别简单
export default function App() {
return (
<Form>
<FormInput
name="username"
label="Name"
rules={[{ required: true, message: 'Please input your name' }]}
placeholder="Please input your name"
/>
<Form.Item
name="username"
label="Name"
rules={[{ required: true, message: 'Please input your name' }]}
>
<Input placeholder="Please input your name" />
</Form.Item>
</Form>
);
}
基本可以说相同的代码,但是封装以后必选的样式就会丢失
于是我就去 antd
的 github
仓库提了一个 issue,顺便体验了一波 stackblitz。在提 issue
的过程中,我逐渐发现了 bug
的问题所在,下面也有社区的大佬们直接贴出了具体代码
可以看出来这个 bug
其实挺简单,就是后面的 props
把前面 isRequired
的值给覆盖了。所以我想着是不是可以混个 pr
,刚 fork
完代码,发现已经有人提交了 pr
,还加上测试用例已经在跑 ci
了。惊了,antd
的社区真的很活跃,而且很多人都很热心,我也很佩服。
总结
通过这次封装组件的经历,我不仅学会了如何使用 React Hooks
和 TypeScript
编写一个具有类型提示的高阶组件,还发现了一个 antd
的 bug
,并提了一个 issue
。虽然没有成功混上 pr
有点可惜,但是下次如果再有这样的机会我肯定会先自己尝试一下,如果还是不行,再去提 issue
。