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

react-hook-form将数字字段识别为字符串的问题求助

问题:Zod验证数字表单时出现「Expected number, received string」错误

我使用Zod进行表单类型验证,规则如下:

const formSchema = z.object({
  numberField: z.number().min(1, "numberField is required"),
});

配合react-hook-form的自定义组件使用,表单无法输入字符串,但提交时仍收到「Expected number, received string」的验证错误。相关组件代码如下:

表单组件代码

<form onSubmit={handleFormOnSubmit} className="space-y-4">   
  <div className="xs:col-span-full sm:col-span-3">
    <NumberField
      name="numberField"
      label="Insert a number"
      control={control}
    />
  </div>
</form>

自定义NumberField组件代码

import React from "react";
import { useController } from "react-hook-form";
import type { FieldValues, UseControllerProps } from "react-hook-form";

import NumberInput from "../../Inputs/NumberInput";
import type { NumberInputProps } from "../../Inputs/NumberInput";

export type NumberFieldProps<P extends FieldValues> = UseControllerProps<P> &
  Omit<NumberInputProps, "name">;

const NumberField = <P extends FieldValues>({
  name,
  control,
  defaultValue,
  ...rest
}: NumberFieldProps<P>) => {
  const {
    field: { onChange, onBlur, value, name: fieldName },
    fieldState: { error },
  } = useController({ name, control, defaultValue });

  return (
    <NumberInput
      name={fieldName}
      onChange={onChange}
      onBlur={onBlur}
      value={value as number}
      error={error?.message}
      {...rest}
    />
  );
};

export default NumberField;

NumberInput组件代码

import React from "react";
import clsx from "clsx";
import type { InputHTMLAttributes } from "react";

export interface NumberInputProps
  extends InputHTMLAttributes<HTMLInputElement> {
  name: string;
  label: string;
  error?: string;
  helperText?: string;
}

const NumberInput: React.FC<NumberInputProps> = ({
  label,
  error,
  helperText,
  className = "",
  name,
  required,
  value,
  ...inputProps
}) => {
  const inputClassName = clsx(
    "bg-gray-800 block border-0 flex-1 focus:ring-2 focus:ring-indigo-600 focus:ring-inset placeholder:text-gray-400 py-1.5 ring-1 ring-inset rounded-md shadow-sm sm:leading-6 sm:text-sm text-white w-full",
    {
      "ring-red-500": error,
      "ring-gray-700": !error,
    },
    className,
  );

  return (
    <div className="w-full">
      <label
        htmlFor={name}
        className="block text-sm font-medium leading-6 text-white"
      >
        {label} {required && <span className="text-red-500">*</span>}
      </label>
      <div className="mt-2">
        <input
          type="number"
          name={name}
          id={name}
          className={inputClassName}
          value={Number(value)}
          {...inputProps}
        />
      </div>
      {error && <p className="mt-1 text-sm text-red-500">{error}</p>}
      {helperText && <p className="mt-1 text-sm text-gray-400">{helperText}</p>}
    </div>
  );
};

export default NumberInput;

问题根源

  • HTML number输入的本质:type="number"的输入框,触发onChange时传递的仍然是字符串类型的值,哪怕输入的是数字。
  • 组件只处理了渲染层转换:NumberInput中仅把value转为Number(value)用于渲染,但没有修改onChange传递给react-hook-form的值类型,导致表单存储的始终是字符串,触发Zod的数字类型校验失败。
  • 默认值缺失:未给NumberField设置数字类型的defaultValue时,react-hook-form会将字段初始值设为undefined,用户首次输入的字符串值直接存入表单,未做转换。

修复方案

1. 在NumberInput中处理onChange,转换值类型

修改NumberInput组件,拦截onChange事件,将输入的字符串转为数字后再传递给react-hook-form:

const NumberInput: React.FC<NumberInputProps> = ({
  label,
  error,
  helperText,
  className = "",
  name,
  required,
  value,
  onChange,
  ...inputProps
}) => {
  const inputClassName = clsx(
    "bg-gray-800 block border-0 flex-1 focus:ring-2 focus:ring-indigo-600 focus:ring-inset placeholder:text-gray-400 py-1.5 ring-1 ring-inset rounded-md shadow-sm sm:leading-6 sm:text-sm text-white w-full",
    {
      "ring-red-500": error,
      "ring-gray-700": !error,
    },
    className,
  );

  const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    // 空值转为undefined,避免NaN
    const numericValue = e.target.value ? Number(e.target.value) : undefined;
    // 传递转换后的数字给react-hook-form的onChange
    onChange?.(numericValue);
  };

  return (
    <div className="w-full">
      <label
        htmlFor={name}
        className="block text-sm font-medium leading-6 text-white"
      >
        {label} {required && <span className="text-red-500">*</span>}
      </label>
      <div className="mt-2">
        <input
          type="number"
          name={name}
          id={name}
          className={inputClassName}
          // 避免渲染NaN,空值显示为空字符串
          value={value !== undefined ? value : ''}
          onChange={handleInputChange}
          {...inputProps}
        />
      </div>
      {error && <p className="mt-1 text-sm text-red-500">{error}</p>}
      {helperText && <p className="mt-1 text-sm text-gray-400">{helperText}</p>}
    </div>
  );
};

2. 给NumberField设置数字类型的默认值(推荐)

使用NumberField时,指定defaultValue为数字,确保初始值类型正确:

<NumberField
  name="numberField"
  label="Insert a number"
  control={control}
  defaultValue={0} // 或根据需求设为null(需同步调整Zod规则)
/>

如果允许字段为空,需更新Zod规则:

const formSchema = z.object({
  numberField: z.number().nullable().min(1, "numberField is required"),
});

3. 可选:在react-hook-form resolver中预处理值

若不想修改组件,可在Zod resolver中添加预处理逻辑,将字符串转为数字后再验证:

import { zodResolver } from "@hookform/resolvers/zod";

const form = useForm({
  resolver: zodResolver(formSchema, {
    preprocess: (values) => ({
      ...values,
      numberField: values.numberField ? Number(values.numberField) : undefined,
    }),
  }),
});

内容的提问来源于stack exchange,提问作者peternovak

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.07 00:44:57