Next.js 14中Shadcn表单结合Server Actions的问题求助
解答
问题1:实现客户端验证+服务端提交的组合
核心思路是先通过react-hook-form完成客户端实时验证,验证通过后再调用Server Action,同时用useFormState同步服务端返回的错误到表单UI。具体实现步骤如下:
- 保留react-hook-form的客户端验证配置,确保用户输入时能得到即时反馈;
- 不直接给
<form>标签设置action属性,改用react-hook-form的handleSubmit方法触发流程:先执行客户端验证,通过后手动构造FormData并调用Server Action; - 调整服务端返回的错误格式为键值对(字段名对应错误信息),方便客户端精准匹配字段;
- 用
useEffect监听服务端返回的错误状态,同步到react-hook-form的错误体系中,让错误信息显示在对应字段的FormMessage组件里。
修改后的代码示例
actions.ts
"use server"; import { formSchema } from "./schema"; export async function onSubmitStudent(prevState: any, formData: FormData) { const data = { firstName: formData.get("firstName"), lastName: formData.get("lastName"), }; const parseResult = formSchema.safeParse(data); if (!parseResult.success) { // 将Zod错误转换为字段-错误信息的键值对,方便客户端匹配 const errors = parseResult.error.issues.reduce((acc, issue) => { const field = issue.path[0] as string; acc[field] = issue.message; return acc; }, {} as Record<string, string>); return { ...prevState, errors, message: "表单验证失败" }; } // 这里处理实际业务逻辑(如写入数据库、调用外部API等) console.log("提交的有效数据:", parseResult.data); return { ...prevState, errors: {}, message: "提交成功" }; }
page.tsx
"use client"; import { useForm, zodResolver } from "@hookform/resolvers/zod"; import { useFormState, useFormStatus } from "react-dom"; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { formSchema } from "./schema"; import { onSubmitStudent } from "./actions"; import type { z } from "zod"; import { useEffect } from "react"; type StudentFormValues = z.infer<typeof formSchema>; const initialState = { message: "", errors: {} as Record<string, string>, }; export default function Page() { const { pending } = useFormStatus(); const [state, formAction] = useFormState(onSubmitStudent, initialState); const form = useForm<StudentFormValues>({ resolver: zodResolver(formSchema), defaultValues: { firstName: "", lastName: "", }, }); // 同步服务端返回的错误到react-hook-form useEffect(() => { if (Object.keys(state.errors).length > 0) { Object.entries(state.errors).forEach(([field, message]) => { form.setError(field as keyof StudentFormValues, { message }); }); } else if (state.message) { // 提交成功后重置表单 form.reset(); } }, [state.errors, state.message, form]); // 客户端验证通过后调用Server Action const handleSubmit = form.handleSubmit(async (data) => { const formData = new FormData(); formData.append("firstName", data.firstName); formData.append("lastName", data.lastName); await formAction(formData); }); return ( <Form {...form}> <form onSubmit={handleSubmit} className="space-y-8"> <FormField control={form.control} name="firstName" render={({ field }) => ( <FormItem> <FormLabel>First Name</FormLabel> <FormControl> <Input {...field} disabled={pending} /> </FormControl> <FormMessage /> </FormItem> )} /> <FormField control={form.control} name="lastName" render={({ field }) => ( <FormItem> <FormLabel>Last Name</FormLabel> <FormControl> <Input {...field} disabled={pending} /> </FormControl> <FormMessage /> </FormItem> )} /> {state.message && <p className="text-sm text-green-600">{state.message}</p>} <Button type="submit" disabled={pending}> {pending ? "提交中..." : "提交"} </Button> </form> </Form> ); }
问题2:客户端onSubmit调用Server Action是否属于服务端调用?
这种方式仍然是服务端调用,和直接设置form标签的action属性本质一致:
- Server Action本身是运行在服务端环境的代码,客户端只是通过函数调用的方式触发它,Next.js会自动处理底层通信(无需手动编写API路由);
- 相比传统API调用,它能更好地和React状态(如
useFormState)结合,自动处理FormData,还能避免跨域问题,同时支持渐进增强(即使JS禁用,也能通过表单默认提交触发); - 唯一区别是触发方式:从浏览器默认的表单提交变成了客户端JS主动调用,但执行逻辑完全在服务端完成。
内容的提问来源于stack exchange,提问作者Hassan Ahmad
相关产品推荐
相关产品推荐

