使用ShadCN/react-hook-form提交关联选择组件出错,如何自定义change函数?
问题解决与组件封装方案
一、解决表单提交分类值为空的问题
你推测的原因完全正确:分类选择器的onValueChange只调用了自定义处理函数,没有同步更新表单字段状态,导致提交时category值为空。同时可以通过移除冗余的useState来简化逻辑:
修改后的核心代码
export const TransactionForm = () => { const form = useForm<z.infer<typeof TransactionSchema>>({ resolver: zodResolver(TransactionSchema), defaultValues: { category: "", subcategory: "", }, }); // 用form.watch替代selectedCategory的useState,实时获取表单中分类的选中值 const selectedCategory = form.watch('category'); // 用useMemo自动计算过滤后的子分类,无需手动维护filteredSubcategories状态 const filteredSubcategories = useMemo(() => { const categoryData = dummyData.categories.find(item => item.name === selectedCategory); return categoryData?.subcategories || []; }, [selectedCategory]); const onSubmit = (values: z.infer<typeof TransactionSchema>) => { console.log(values); }; return ( <CardWrapper> <Form {...form}> <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4"> <div className="space-y-4"> <FormField control={form.control} name="category" render={({ field }) => ( <FormItem> <FormLabel>Category:</FormLabel> <Select // 自定义逻辑+同步表单字段 onValueChange={(value) => { field.onChange(value); // 切换分类时自动清空子分类 form.setValue('subcategory', ''); }} value={field.value} > <FormControl> <SelectTrigger className="w-[180px] h-[40px]"> <SelectValue placeholder="Category" /> </SelectTrigger> </FormControl> <SelectContent className="bg-white"> <SelectGroup> <SelectLabel>Category</SelectLabel> {dummyData.categories.map((option, index) => ( <SelectItem key={index} value={option.name}> {option.name} </SelectItem> ))} </SelectGroup> </SelectContent> </Select> </FormItem> )} /> <FormField control={form.control} name="subcategory" render={({ field }) => ( <FormItem> <FormLabel>Subcategory:</FormLabel> <Select onValueChange={field.onChange} value={field.value} // 未选择分类时禁用子分类 disabled={!selectedCategory} > <FormControl> <SelectTrigger className="w-[180px] h-[40px]"> <SelectValue placeholder="Subcategory" /> </SelectTrigger> </FormControl> <SelectContent className="bg-white"> <SelectGroup> <SelectLabel>Subcategory</SelectLabel> {filteredSubcategories.map((option, index) => ( <SelectItem key={index} value={option}> {option} </SelectItem> ))} </SelectGroup> </SelectContent> </Select> </FormItem> )} /> </div> <Button type="submit" className="w-full"> Save </Button> </form> </Form> </CardWrapper> ); };
二、封装可复用的依赖选择组件
将两个关联的Select封装成独立组件,支持自定义字段名、标签和分类数据,避免重复代码:
封装后的组件代码
import { useFormContext, Control } from 'react-hook-form'; import { FormField, FormItem, FormLabel, FormControl } from '@/components/ui/form'; import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue } from '@/components/ui/select'; import { useMemo } from 'react'; // 定义分类数据类型 type Category = { name: string; subcategories: string[]; }; // 组件props类型 type DependentSelectsProps = { categories: Category[]; control?: Control<Record<string, string>>; categoryFieldName?: string; subcategoryFieldName?: string; labels?: { category: string; subcategory: string; }; }; export const DependentSelects = ({ categories, control, categoryFieldName = 'category', subcategoryFieldName = 'subcategory', labels = { category: 'Category', subcategory: 'Subcategory' }, }: DependentSelectsProps) => { // 优先使用传入的control,否则从FormContext中获取 const formControl = control || useFormContext().control; const selectedCategory = formControl._formState.watch[categoryFieldName]; // 自动计算过滤后的子分类 const filteredSubcategories = useMemo(() => { const categoryData = categories.find(item => item.name === selectedCategory); return categoryData?.subcategories || []; }, [selectedCategory, categories]); return ( <div className="space-y-4"> <FormField control={formControl} name={categoryFieldName} render={({ field }) => ( <FormItem> <FormLabel>{labels.category}:</FormLabel> <Select onValueChange={(value) => { field.onChange(value); formControl.setValue(subcategoryFieldName, ''); }} value={field.value} > <FormControl> <SelectTrigger className="w-[180px] h-[40px]"> <SelectValue placeholder={labels.category} /> </SelectTrigger> </FormControl> <SelectContent className="bg-white"> <SelectGroup> <SelectLabel>{labels.category}</SelectLabel> {categories.map((option, index) => ( <SelectItem key={index} value={option.name}> {option.name} </SelectItem> ))} </SelectGroup> </SelectContent> </Select> </FormItem> )} /> <FormField control={formControl} name={subcategoryFieldName} render={({ field }) => ( <FormItem> <FormLabel>{labels.subcategory}:</FormLabel> <Select onValueChange={field.onChange} value={field.value} disabled={!selectedCategory} > <FormControl> <SelectTrigger className="w-[180px] h-[40px]"> <SelectValue placeholder={labels.subcategory} /> </SelectTrigger> </FormControl> <SelectContent className="bg-white"> <SelectGroup> <SelectLabel>{labels.subcategory}</SelectLabel> {filteredSubcategories.map((option, index) => ( <SelectItem key={index} value={option}> {option} </SelectItem> ))} </SelectGroup> </SelectContent> </Select> </FormItem> )} /> </div> ); };
在表单中使用封装组件
export const TransactionForm = () => { const form = useForm<z.infer<typeof TransactionSchema>>({ resolver: zodResolver(TransactionSchema), defaultValues: { category: "", subcategory: "", }, }); const onSubmit = (values: z.infer<typeof TransactionSchema>) => { console.log(values); }; return ( <CardWrapper> <Form {...form}> <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4"> <DependentSelects categories={dummyData.categories} control={form.control} labels={{ category: 'Category', subcategory: 'Subcategory' }} /> <Button type="submit" className="w-full"> Save </Button> </form> </Form> </CardWrapper> ); };
内容的提问来源于stack exchange,提问作者everspader
相关产品推荐
相关产品推荐

