You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.03 00:30:39