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
相关产品推荐
相关产品推荐

