Next.js中react-hook-form的客户端服务端处理及错误注入问题
问题解决:Next.js中react-hook-form与服务端Action的冲突及服务端ZodError注入方案
一、为什么加onSubmit={form.handleSubmit(...)}后服务端Action不触发?
react-hook-form的handleSubmit默认会调用event.preventDefault(),拦截原生表单的提交行为,导致你设置的action={RegisterAccount}无法触发。解决思路有两种:手动触发原生提交,或者结合Next.js的useActionState绑定服务端状态。
二、方案1:手动触发原生提交(快速兼容)
在handleSubmit的回调里,手动调用原生表单的提交方法,既保留客户端验证,又触发服务端Action:
'use client'; import { useForm, zodResolver } from 'react-hook-form'; import { z } from 'zod'; import { RegisterAccount } from '@/app/actions/register'; import { registerFormSchema } from '@/schemas'; type FormValues = z.infer<typeof registerFormSchema>; export default function RegisterForm() { const form = useForm<FormValues>({ resolver: zodResolver(registerFormSchema), defaultValues: { email: '', password: '' } }); const onSubmit = (data, e) => { // 这里可以加客户端提交前的逻辑,比如设置loading状态 e.target.submit(); // 手动触发原生表单提交,调用服务端Action }; return ( <form action={RegisterAccount} onSubmit={form.handleSubmit(onSubmit)}> <div> <label>邮箱</label> <input {...form.register('email')} /> {form.formState.errors.email && <p>{form.formState.errors.email.message}</p>} </div> <div> <label>密码</label> <input type="password" {...form.register('password')} /> {form.formState.errors.password && <p>{form.formState.errors.password.message}</p>} </div> <button type="submit">注册</button> </form> ); }
三、方案2:结合useActionState注入服务端ZodError(推荐)
这个方案能直接把服务端返回的ZodError同步到react-hook-form的错误状态里,完美整合客户端与服务端验证:
1. 改造服务端Action函数
让服务端函数返回包含错误信息的状态对象:
// app/actions/register.ts import { z } from 'zod'; import { registerFormSchema } from '@/schemas'; // 定义返回状态类型 type ActionState = { error?: z.ZodError; }; export async function RegisterAccount(prevState: ActionState, formData: FormData): Promise<ActionState> { try { // 把FormData转成对象后用zod解析 const formValues = Object.fromEntries(formData); registerFormSchema.parse(formValues); // 执行注册逻辑(比如存数据库、发验证邮件等) return { error: undefined }; } catch (err) { // 捕获ZodError并返回 if (err instanceof z.ZodError) { return { error: err }; } // 处理其他错误 return { error: new z.ZodError([{ message: '未知错误', path: [] }]) }; } }
2. 客户端组件整合useActionState与react-hook-form
'use client'; import { useForm, zodResolver } from 'react-hook-form'; import { z } from 'zod'; import { useActionState, useEffect } from 'react'; import { RegisterAccount } from '@/app/actions/register'; import { registerFormSchema } from '@/schemas'; type FormValues = z.infer<typeof registerFormSchema>; type ActionState = Awaited<ReturnType<typeof RegisterAccount>>; export default function RegisterForm() { // 用useActionState绑定服务端Action和状态 const [actionState, formAction] = useActionState(RegisterAccount, { error: undefined }); const form = useForm<FormValues>({ resolver: zodResolver(registerFormSchema), defaultValues: { email: '', password: '' } }); // 监听服务端返回的错误,同步到react-hook-form useEffect(() => { if (actionState.error) { actionState.error.issues.forEach(issue => { // 把ZodError的path映射到表单字段,注入错误信息 form.setError(issue.path.join('.') as keyof FormValues, { message: issue.message }); }); } }, [actionState.error, form]); return ( <form action={formAction} onSubmit={form.handleSubmit(() => {})}> <div> <label>邮箱</label> <input {...form.register('email')} /> {form.formState.errors.email && <p>{form.formState.errors.email.message}</p>} </div> <div> <label>密码</label> <input type="password" {...form.register('password')} /> {form.formState.errors.password && <p>{form.formState.errors.password.message}</p>} </div> <button type="submit">注册</button> </form> ); }
关键说明
- 客户端验证(react-hook-form+zod)负责即时反馈,提升用户体验;服务端验证是最后一道防线,防止恶意提交。
useActionState会自动管理服务端Action的返回状态,每次提交后actionState会更新为服务端返回的结果。- 通过
form.setError可以精准把服务端返回的字段错误映射到表单对应的输入框上。
内容的提问来源于stack exchange,提问作者ampersanded
相关产品推荐
相关产品推荐

