React Hook Form提交前结合Zod校验MongoDB中recipeId唯一性的最佳实践
前端预校验MongoDB唯一性(React Hook Form + Zod)最佳实践
核心思路
前端通过调用后端提供的唯一性检查接口,结合Zod的异步验证能力,在表单提交前完成校验,并将错误信息以Zod原生提示的形式反馈给用户。同时保留后端的唯一性校验作为兜底(防止前端校验被绕过)。
步骤1:后端提供唯一性检查接口
首先需要在后端实现一个接口,用于查询指定recipeId是否已存在。示例接口逻辑(以Express为例):
// 后端路由:/api/check-recipe-id-unique app.get('/api/check-recipe-id-unique', async (req, res) => { const { id } = req.query; if (!id) return res.json({ isUnique: false }); const exists = await Recipe.findOne({ recipeId: id }); res.json({ isUnique: !exists }); });
步骤2:编写异步Zod验证器
在前端实现一个异步函数,调用上述接口,并通过Zod的refine方法将其绑定到recipeId字段:
import { z } from 'zod'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; // 防抖处理:避免用户输入时频繁发请求(可选但推荐) const debounce = <T extends (...args: any[]) => Promise<any>>(fn: T, delay: number) => { let timeoutId: NodeJS.Timeout; return (...args: Parameters<T>) => { clearTimeout(timeoutId); return new Promise<ReturnType<T>>((resolve) => { timeoutId = setTimeout(async () => { const result = await fn(...args); resolve(result); }, delay); }); }; }; // 调用后端接口检查唯一性 const checkRecipeIdUnique = async (recipeId: string): Promise<boolean> => { const response = await fetch(`/api/check-recipe-id-unique?id=${encodeURIComponent(recipeId)}`); const data = await response.json(); return data.isUnique; }; // 给检查函数加防抖 const debouncedCheck = debounce(checkRecipeIdUnique, 500); // 修改Zod Schema,添加异步验证 const FormSchema = z.object({ recipeId: z.string().refine( async (id) => await debouncedCheck(id), { message: "该recipeId已被占用" } ), name: z .string() .min(4, { message: "名称长度至少为4个字符" }), description: z.string(), });
步骤3:配置React Hook Form支持异步验证
在初始化useForm时,指定Zod解析器,并设置验证触发时机(按需选择):
const { register, handleSubmit, formState: { errors, isSubmitting } } = useForm<z.infer<typeof FormSchema>>({ resolver: zodResolver(FormSchema), mode: "onBlur", // 可选:onChange(输入时实时校验)/ onSubmit(提交时校验) }); const onSubmit = async (data: z.infer<typeof FormSchema>) => { // 提交逻辑:调用后端创建食谱接口 };
步骤4:渲染表单与错误提示
在表单中绑定字段,并显示Zod返回的错误信息:
<form onSubmit={handleSubmit(onSubmit)}> <div> <label>Recipe ID</label> <input {...register("recipeId")} type="text" /> {errors.recipeId && <span style={{ color: 'red' }}>{errors.recipeId.message}</span>} </div> <div> <label>名称</label> <input {...register("name")} type="text" /> {errors.name && <span style={{ color: 'red' }}>{errors.name.message}</span>} </div> <div> <label>描述</label> <textarea {...register("description")} /> </div> <button type="submit" disabled={isSubmitting}>提交</button> </form>
关键注意事项
- 后端兜底校验不可少:前端校验仅优化用户体验,恶意请求可能绕过前端直接调用后端接口,因此必须保留原有的MongoDB Schema唯一性校验(如
unique: true或中间件)。 - 防抖优化:避免用户输入过程中频繁发起请求,减少后端压力。
- 错误处理:考虑接口请求失败的情况(如网络错误),可在异步验证函数中添加错误捕获,返回友好提示。
内容的提问来源于stack exchange,提问作者lexer
相关产品推荐
相关产品推荐

