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

Next.js 14中Shadcn表单结合Server Actions的问题求助

解答

问题1:实现客户端验证+服务端提交的组合

核心思路是先通过react-hook-form完成客户端实时验证,验证通过后再调用Server Action,同时用useFormState同步服务端返回的错误到表单UI。具体实现步骤如下:

  1. 保留react-hook-form的客户端验证配置,确保用户输入时能得到即时反馈;
  2. 不直接给<form>标签设置action属性,改用react-hook-form的handleSubmit方法触发流程:先执行客户端验证,通过后手动构造FormData并调用Server Action;
  3. 调整服务端返回的错误格式为键值对(字段名对应错误信息),方便客户端精准匹配字段;
  4. 用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.24 16:24:54