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

