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

如何在React中结合Zod与React Hook Form实现对象数组复选框

优化React Hook Form + Zod实现附加服务复选框控制方案

你的现有实现存在几个问题:

  • 直接操作addOns.0索引会覆盖数组元素,无法支持多个附加服务同时选中
  • 未处理取消勾选时从数组移除对应项的逻辑
  • register("addOns")和setValue混用容易导致表单状态不一致,且多个复选框共用同一个name会引发冲突

以下是更简洁、可维护的实现方案:

1. 定义附加服务配置(避免重复代码)

先把所有附加服务的元数据抽离成配置数组,方便后续渲染和逻辑处理:

const addOnConfigs = [
  { name: "Online service", monthlyPrice: "1", yearlyPrice: "10" },
  { name: "Larger storage", monthlyPrice: "2", yearlyPrice: "20" },
  { name: "Customizable Storage", monthlyPrice: "2", yearlyPrice: "20" },
];

2. 实现复选框的勾选/取消逻辑

利用React Hook Form的setValue和watch来管理addOns数组状态,每个复选框单独处理自身的勾选事件:

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

// 保持你的Zod Schema不变
const formSchema = z.object({
  billing: z.enum(["Monthly", "Yearly"]),
  addOns: z.array(
    z.union([
      z.object({
        name: z.literal("Online service"),
        price: z.enum(["1", "10"]),
      }),
      z.object({
        name: z.literal("Larger storage"),
        price: z.enum(["2", "20"]),
      }),
      z.object({
        name: z.literal("Customizable Storage"),
        price: z.enum(["2", "20"]),
      }),
    ])
  ),
});

type FormData = z.infer<typeof formSchema>;

export default function AddOnForm() {
  const { register, setValue, watch } = useForm<FormData>({
    resolver: zodResolver(formSchema),
    defaultValues: { 
      billing: "Monthly",
      addOns: [] 
    },
  });

  const billing = watch("billing");
  const currentAddOns = watch("addOns");

  // 处理单个附加服务的勾选/取消
  const handleAddOnToggle = (addOnConfig: typeof addOnConfigs[0]) => {
    const isSelected = currentAddOns.some(item => item.name === addOnConfig.name);
    
    if (isSelected) {
      // 取消勾选:移除对应项
      setValue("addOns", currentAddOns.filter(item => item.name !== addOnConfig.name));
    } else {
      // 勾选:添加对应项,根据计费方式选择价格
      const price = billing === "Monthly" ? addOnConfig.monthlyPrice : addOnConfig.yearlyPrice;
      setValue("addOns", [...currentAddOns, { name: addOnConfig.name, price }]);
    }
  };

  // 监听计费方式变化,自动更新已选附加服务的价格
  useEffect(() => {
    setValue("addOns", currentAddOns.map(item => {
      const matchConfig = addOnConfigs.find(c => c.name === item.name);
      return {
        ...item,
        price: billing === "Monthly" ? matchConfig?.monthlyPrice : matchConfig?.yearlyPrice,
      };
    }));
  }, [billing, currentAddOns, setValue]);

  return (
    <form>
      {/* 计费方式选择(示例) */}
      <div className="mb-4">
        <input
          type="radio"
          id="monthly"
          {...register("billing")}
          value="Monthly"
        />
        <label htmlFor="monthly">月度计费</label>
        
        <input
          type="radio"
          id="yearly"
          {...register("billing")}
          value="Yearly"
          className="ml-4"
        />
        <label htmlFor="yearly">年度计费</label>
      </div>

      {/* 渲染所有附加服务复选框 */}
      {addOnConfigs.map(config => (
        <div key={config.name} className="flex items-center gap-3 mb-2">
          <input
            type="checkbox"
            id={config.name.toLowerCase().replace(/\s+/g, "-")}
            checked={currentAddOns.some(item => item.name === config.name)}
            onChange={() => handleAddOnToggle(config)}
            className="rounded-checkbox checked:accent-purplish-blue w-5 h-5 border-[1px] border-light-gray border-solid"
          />
          <label htmlFor={config.name.toLowerCase().replace(/\s+/g, "-")}>
            {config.name}
            <span className="text-sm text-gray-500 ml-2">
              ({billing === "Monthly" ? `+$${config.monthlyPrice}/月` : `+$${config.yearlyPrice}/年`})
            </span>
          </label>
        </div>
      ))}

      <button type="submit" className="mt-4 px-4 py-2 bg-blue-500 text-white rounded">
        提交
      </button>
    </form>
  );
}

3. 方案优势

  • 可维护性:通过配置数组统一管理附加服务信息,新增/修改服务只需修改配置
  • 状态一致性:通过watch实时获取当前addOns状态,避免手动操作索引导致的错误
  • 自动同步价格:监听计费方式变化,自动更新已选附加服务的价格,无需用户重新勾选
  • 符合Zod验证:每次更新都严格按照Schema生成对象,确保表单数据始终符合验证规则

注意事项

  • 确保billing字段已在表单中正确注册并管理
  • 如果不需要自动更新价格,可以移除useEffect部分,仅保留勾选/取消逻辑

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.01 18:01:17