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

Next.js+Chakra UI中React Hook Form Zod验证react-select失效问题

问题解决:React Hook Form + Zod 验证 react-select 错误提示不消失

核心问题根源

react-select 属于非标准表单组件,无法直接通过 RHF 的 register 方法绑定状态,且其返回的选中值格式(通常是 {value: string, label: string})与普通 input 不一致,导致 Zod 验证状态未同步更新。


分步解决方案

1. 重构下拉组件:用 Controller 接管状态

在封装的 DropdownInput.tsx 中,必须使用 RHF 的 Controller 组件包裹 react-select,确保表单状态正确联动:

import { Controller, useFormContext } from 'react-hook-form';
import { FormControl, FormLabel, FormErrorMessage } from '@chakra-ui/react';
import RawDropdownInput from './RawDropdownInput';

type DropdownInputProps = {
  name: string;
  label: string;
  loadOptions: (inputValue: string) => Promise<any[]>;
  // 其他 react-select 所需 props
};

const DropdownInput = ({ name, label, ...rest }: DropdownInputProps) => {
  const { control, formState: { errors } } = useFormContext();
  const hasError = !!errors[name];

  return (
    <FormControl isInvalid={hasError}>
      <FormLabel>{label}</FormLabel>
      <Controller
        name={name}
        control={control}
        // 确保默认值为 null(而非 undefined),避免验证逻辑异常
        defaultValue={null}
        render={({ field: { onChange, value } }) => (
          <RawDropdownInput
            {...rest}
            value={value}
            // 直接将选中的选项传递给 RHF 的 onChange,更新表单状态
            onChange={onChange}
          />
        )}
      />
      <FormErrorMessage>{errors[name]?.message}</FormErrorMessage>
    </FormControl>
  );
};

export default DropdownInput;

2. 基础下拉组件:保持状态透传

RawDropdownInput.tsx 只需简单透传 props,无需处理表单逻辑:

import AsyncSelect from 'react-select/async';

type OptionType = { value: string; label: string };

type RawDropdownInputProps = {
  value: OptionType | null;
  onChange: (option: OptionType | null) => void;
  loadOptions: (inputValue: string) => Promise<OptionType[]>;
};

const RawDropdownInput = ({ value, onChange, loadOptions }: RawDropdownInputProps) => {
  return (
    <AsyncSelect
      value={value}
      onChange={onChange}
      loadOptions={loadOptions}
      isClearable
    />
  );
};

export default RawDropdownInput;

3. 调整 Zod 验证规则

根据 react-select 返回的 value 结构,编写匹配的验证逻辑。如果要求必须选择选项,用 refine 确保选中值不为 null:

import { z } from 'zod';

const FormSchema = z.object({
  // 匹配 react-select 返回的 {value, label} 结构
  category: z
    .object({ value: z.string(), label: z.string() })
    .nullable()
    .refine(val => val !== null, {
      message: '请选择一个选项'
    })
});

如果只需要验证选中值的 value 字段,可在 Controller 中转换值:

<Controller
  name="category"
  control={control}
  defaultValue=""
  // 将选中选项的 value 提取出来,传递给表单状态
  valueAs={(val) => val?.value || ''}
  render={({ field: { onChange, value } }) => (
    <RawDropdownInput
      {...rest}
      // 将表单状态的 value 转换回 react-select 所需的格式
      value={value ? { value, label: '' } : null}
      onChange={(option) => onChange(option?.value || '')}
    />
  )}
/>

对应的 Zod 规则可简化为:

const FormSchema = z.object({
  category: z.string().min(1, '请选择一个选项')
});

4. 确保表单容器正确传递上下文

AppForm.tsx 中需用 FormProvider 传递 RHF 上下文,并使用 Zod 解析器:

import { FormProvider, useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';

type AppFormProps<T extends z.ZodType<any>> = {
  schema: T;
  onSubmit: (data: z.infer<T>) => void;
  children: React.ReactNode;
};

const AppForm = <T extends z.ZodType<any>>({ schema, onSubmit, children }: AppFormProps<T>) => {
  const form = useForm({
    resolver: zodResolver(schema),
    // 统一设置默认值,避免字段为 undefined
    defaultValues: { category: null }
  });

  return (
    <FormProvider {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)}>
        {children}
        <button type="submit" mt={4}>提交</button>
      </form>
    </FormProvider>
  );
};

export default AppForm;

5. 页面使用示例

form.tsx 中正常调用组件即可:

import AppForm from '../components/AppForm';
import DropdownInput from '../components/DropdownInput';
import { z } from 'zod';

const FormSchema = z.object({
  category: z
    .object({ value: z.string(), label: z.string() })
    .nullable()
    .refine(val => val !== null, { message: '请选择分类' })
});

const FormPage = () => {
  const handleSubmit = (data: z.infer<typeof FormSchema>) => {
    console.log('提交数据:', data);
  };

  // 模拟异步加载选项
  const loadCategories = async (inputValue: string) => {
    const res = await fetch(`/api/categories?keyword=${inputValue}`);
    return res.json();
  };

  return (
    <AppForm schema={FormSchema} onSubmit={handleSubmit}>
      <DropdownInput
        name="category"
        label="分类"
        loadOptions={loadCategories}
      />
    </AppForm>
  );
};

export default FormPage;

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.21 18:23:15