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

使用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.24 19:24:52