如何用Mantine UI和React Hook Form(TypeScript)创建包装组件?
问题描述
我正在使用Mantine UI和React Hook Form(简称RHF)开发自定义表单组件,通过RHF的Controller组件自动注册输入项。发现TextInput、DatePicker等组件结构高度重复,尝试用高阶组件(HOC)抽象复用代码,但始终无法正确处理TypeScript类型,出现类型不兼容报错。核心问题有两个:
- Mantine组件的forward ref类型处理
- 不同组件onChange的类型差异
代码示例
TextInput组件代码
import { FieldValues, Controller, RegisterOptions, Path } from "react-hook-form"; import { MantineTextInput } from "@mantine/core"; export type RegisteredFormComponentProps<T extends FieldValues> = { name: Path<T>; // 类型来自React Hook Form rules?: RegisterOptions; }; type TextInputProps<T extends FieldValues> = Omit< ComponentProps<typeof MantineTextInput>, 'name' > & RegisteredFormComponentProps<T>; const TextInput = <T extends FieldValues>({ name, ...otherProps }: TextInputProps<T>) => { return ( <Controller name={name} rules={{ required: '必填', }} render={({ field: { name, value, ref, onChange }, fieldState: { error }, }) => ( <MantineTextInput {...otherProps} name={name} error={error?.message} value={value} styles={(theme) => ({ label: { color: error ? theme.colors.red[5] : theme.colors.main[5], }, })} ref={ref} onChange={(e) => { onChange(e.target.value); }} /> )} /> ); };
DatePicker组件代码
import { FieldValues, Controller, Path } from "react-hook-form"; import { MantineDatePicker } from "@mantine/dates"; type DatePickerProps<T extends FieldValues> = Omit< ComponentProps<typeof MantineDatePicker>, 'name' > & RegisteredFormComponentProps<T>; const DatePicker = <T extends FieldValues>({ name, ...otherProps }: DatePickerProps<T>) => { return ( <Controller name={name} render={({ field: { name, value, ref, onChange }, fieldState: { error }, }) => ( <MantineDatePicker {...otherProps} error={error?.message} name={name} dropdownPosition="bottom-start" styles={(theme) => ({ label: { color: error ? theme.colors.red[5] : theme.colors.main[5], }, })} ref={ref} value={value} onChange={(date) => { onChange(date); }} /> )} /> ); };
尝试的HOC代码
const withForm = < T extends FieldValues, C extends typeof MantineDatePicker | typeof MantineNumberInput >( Component: C ) => ({ name, rules, ...otherProps }: Omit<ComponentProps<C>, 'name'> & RegisteredFormComponentProps<T>) => { return ( <Controller name={name} rules={rules} render={({ field: { name, value, ref, onChange }, fieldState: { error }, }) => ( <Component {...otherProps} error={error?.message} name={name} dropdownPosition="bottom-start" styles={(theme) => ({ label: { color: error ? theme.colors.red[5] : theme.colors.main[5], }, })} ref={ref} value={value} onChange={(date) => { onChange(date); }} /> )} /> ); };
报错信息(中文翻译)
类型 'Omit<Omit<ComponentProps
, "name"> & RegisteredFormComponentProps , "name" | "rules"> & { error: string | undefined; ... 5 more ...; onChange: (date: number | ... 2 more ... | undefined) => void; }' 无法分配给类型 'IntrinsicAttributes & LibraryManagedAttributes<C, DatePickerProps & RefAttributes & NumberInputProps>'。
解决方案
1. 重构HOC,兼容Forward Ref与动态OnChange类型
我们需要通过泛型精准捕获Mantine组件的Props和Ref类型,同时提取不同组件的onChange参数类型,避免硬编码导致的类型冲突:
import { FieldValues, Controller, RegisterOptions, Path } from "react-hook-form"; import { ComponentProps, ForwardRefExoticComponent, RefAttributes } from "react"; // 复用的表单注册Props export type RegisteredFormComponentProps<T extends FieldValues> = { name: Path<T>; rules?: RegisterOptions; }; // 提取组件onChange的参数类型工具 type ExtractOnChangeParam<T> = T extends { onChange: (param: infer P) => void } ? P : never; // 通用表单HOC const withForm = < T extends FieldValues, C extends ForwardRefExoticComponent<ComponentProps<C> & RefAttributes<any>> >( Component: C, // 组件个性化配置:支持默认Props和onChange值映射 componentConfig?: { defaultProps?: Partial<ComponentProps<C>>; mapOnChange?: (value: ExtractOnChangeParam<ComponentProps<C>>) => any; } ) => { return ({ name, rules, ...otherProps }: Omit<ComponentProps<C>, "name"> & RegisteredFormComponentProps<T>) => { return ( <Controller name={name} rules={rules} render={({ field: { name, value, ref, onChange }, fieldState: { error } }) => { // 统一处理onChange的类型转换 const handleChange = (param: ExtractOnChangeParam<ComponentProps<C>>) => { const mappedValue = componentConfig?.mapOnChange ? componentConfig.mapOnChange(param) : param; onChange(mappedValue); }; return ( <Component {...otherProps} {...componentConfig?.defaultProps} name={name} error={error?.message} styles={(theme) => ({ label: { color: error ? theme.colors.red[5] : theme.colors.main[5], }, })} ref={ref} value={value} onChange={handleChange} /> ); }} /> ); }; };
2. 使用HOC生成表单组件
现在可以快速生成适配不同Mantine组件的表单输入项,自动处理类型差异:
import { MantineTextInput } from "@mantine/core"; import { MantineDatePicker } from "@mantine/dates"; // 生成带表单逻辑的TextInput const FormTextInput = withForm<FieldValues, typeof MantineTextInput>(MantineTextInput, { mapOnChange: (e) => e.target.value // 处理input的ChangeEvent类型 }); // 生成带表单逻辑的DatePicker const FormDatePicker = withForm<FieldValues, typeof MantineDatePicker>(MantineDatePicker, { defaultProps: { dropdownPosition: "bottom-start" } // 注入默认属性 });
关键改进点
- 用
ForwardRefExoticComponent明确兼容Mantine的forward ref组件,解决ref类型不匹配问题 - 通过
ExtractOnChangeParam泛型工具动态提取组件的onChange参数类型,避免硬编码 - 增加
componentConfig参数,支持不同组件的个性化配置(默认Props、值映射) - 类型推导更精准,TS可自动识别每个组件的Props和事件类型
内容的提问来源于stack exchange,提问作者Raph117
相关产品推荐
相关产品推荐

