文章
nextjs 表单操作一般步骤
以注册功能表单为例
1、工具
lib/uitls.ts
import {clsx, type ClassValue} from "clsx"
import {twMerge} from "tailwind-merge"
import bcrypt from "bcryptjs";
const cn = (...inputs: ClassValue[]) => {
return twMerge(clsx(inputs));
};
const toStringSafe = (
value: string | number | null | undefined | unknown
): string => {
return value === null ? "" : String(value);
};
const toNumberSafe = (value: string | number | null | undefined): number => {
if (value == null) return 0;
if (typeof value === "number") return value;
const parsed = Number(value);
return isNaN(parsed) ? 0 : parsed;
};
const SALT_ROUNDS = 10;
const hashPassword = async (password: string) => {
return await bcrypt.hash(password, SALT_ROUNDS);
};
export {cn, toStringSafe, toNumberSafe, hashPassword};lib/zodSchema.ts
import { patterns } from "@/lib/constants";
import { z } from "zod";
const regexSchema = (pattern: RegExp) => z.coerce.string().regex(pattern);
const requiredStringSchema = z.string().min(1).max(255).trim();
const passwordSchema = z
.string()
.max(255)
.refine((str) => patterns.minimumOneUpperCaseLetter.test(str), {
message: "Minimum one upper case letter",
})
.refine((str) => patterns.minimumOneLowerCaseLetter.test(str), {
message: "Minimum one lower case letter",
})
.refine((str) => patterns.minimumOneDigit.test(str), {
message: "Minimum one digit",
})
.refine((str) => patterns.minimumOneSpecialCharacter.test(str), {
message: "Minimum one special character",
})
.refine((str) => patterns.minEightCharacters.test(str), {
message: "Minimum eight characters",
});
export { regexSchema, requiredStringSchema, passwordSchema };
2、定义 Schema 和类型
src/app/(auth)/sign-up/_types/signUpSchema.ts
import z from "zod";
import {passwordSchema, requiredStringSchema} from "@/lib/zodSchemas";
const signUpSchema = z
.object({
name: requiredStringSchema,
email: requiredStringSchema,
password: passwordSchema,
confirmPassword: z.string(),
})
.refine((data) => data.password === data.confirmPassword, {
message: "Password don't match",
path: ["confirmPassword"],
});
type SignUpSchema = z.infer<typeof signUpSchema>;
const signUpDefaultValues: SignUpSchema = {
name: "",
email: "",
password: "",
confirmPassword: "",
};
export {signUpDefaultValues, signUpSchema, type SignUpSchema}2、定义 Server Action
src/app/(auth)/sign-up/_services/mutations.ts
"use server";
import {executeAction} from "@/lib/executeAction";
import {hashPassword} from "@/lib/utils";
import {SignUpSchema, signUpSchema} from "@/app/(auth)/sign-up/_types/signUpSchema";
import db from "@/lib/client";
const signUp = async (data: SignUpSchema) => {
await executeAction({
actionFn: async () => {
const validatedData = signUpSchema.parse(data);
const hashedPassword = await hashPassword(validatedData.password);
await db.user.create({
data: {
name: validatedData.name,
email: validatedData.email,
password: hashedPassword,
role: "USER",
},
});
},
});
};
export {signUp};
3、定义 Hook(useSignUp)
src/app/(auth)/sign-up/_services/use-sign-up-mutations.ts
import {useRouter} from "next/navigation";
import {useMutation} from "@tanstack/react-query";
import {SignUpSchema} from "@/app/(auth)/sign-up/_types/signUpSchema";
import {signUp} from "@/app/(auth)/sign-up/_services/mutations";
import {toast} from "sonner";
const useSignUp = () => {
const router = useRouter();
return useMutation({
mutationFn: async (data: SignUpSchema) => {
await signUp(data);
},
onSuccess: () => {
toast.success("Signed up successfully");
router.replace("/sign-in");
},
});
};
export {useSignUp};
4、定义UI组件
src/app/(auth)/sign-up/_components/sign-up-form.tsx
"use client";
import {FormProvider, SubmitHandler, useForm} from "react-hook-form";
import {signUpDefaultValues, signUpSchema, SignUpSchema} from "@/app/(auth)/sign-up/_types/signUpSchema";
import {zodResolver} from "@hookform/resolvers/zod";
import {useSignUp} from "@/app/(auth)/sign-up/_services/use-sign-up-mutations";
import {ControlledInput} from "@/components/ui/controlled-input";
import {Button} from "@/components/ui/button";
import Link from "next/link";
const SignUpForm = () => {
const form = useForm<SignUpSchema>({
defaultValues: signUpDefaultValues,
resolver: zodResolver(signUpSchema),
});
const signUpMutation = useSignUp();
const onSubmit: SubmitHandler<SignUpSchema> = (data) => {
signUpMutation.mutate(data);
};
return (
<FormProvider {...form}>
<form
className="w-full max-w-96 space-y-5 rounded-md border px-10 py-12"
onSubmit={form.handleSubmit(onSubmit)}
>
<div className="text-center">
<h2 className="mb-1 text-2xl font-semibold">Create Account</h2>
<p className="text-muted-foreground text-sm">
Sign up to get started
</p>
</div>
<div className="space-y-3">
<ControlledInput<SignUpSchema> name="name" label="Full Name" />
<ControlledInput<SignUpSchema> name="email" label="Email" />
<ControlledInput<SignUpSchema>
name="password"
label="Password"
type="password"
/>
<ControlledInput<SignUpSchema>
name="confirmPassword"
label="Confirm Password"
type="password"
/>
</div>
<Button className="w-full" isLoading={signUpMutation.isPending}>
Sign Up
</Button>
<div className="text-center text-sm">
Already have an account?{" "}
<Link
href="/sign-in"
className="text-primary font-medium hover:underline"
>
Sign in
</Link>
</div>
</form>
</FormProvider>
);
};
export { SignUpForm };✅ Next.js 表单开发标准化流程(5 层架构)
| 层级 | 职责 | 文件示例 | 关键技术 |
|---|---|---|---|
| 1. 工具层(Utils) | 提供通用工具函数(格式转换、密码哈希、样式合并等) | lib/utils.ts | clsx+twMerge、bcryptjs、安全转换函数 |
| 2. 数据验证层(Schema & Types) | 定义 Zod 表单 Schema + TypeScript 类型 + 默认值 | signUpSchema.ts | zod、z.infer、.refine()做复杂校验 |
| 3. 服务层(Server Action) | 在服务端执行业务逻辑(验证、数据库操作) | mutations.ts | "use server"、executeAction封装错误/事务、Prisma |
| 4. 数据获取层(Hooks) | 封装客户端数据交互逻辑(调用 Server Action + 处理响应) | use-sign-up-mutations.ts | useMutation(TanStack Query)、useRouter、toast |
| 5. UI 层(Components) | 渲染表单界面,绑定状态与提交逻辑 | sign-up-form.tsx | react-hook-form+zodResolver、FormProvider、受控组件 |
🔍 流程执行顺序(从用户操作到数据落库)
- 用户填写表单 →
react-hook-form控制字段状态 + 实时 Zod 验证 - 点击提交 →
form.handleSubmit(onSubmit)触发 - 调用自定义 Hook →
useSignUp().mutate(data) - Hook 调用 Server Action →
signUp(data)(走use server) - Server Action:
- 用 Zod 再次校验(防绕过前端)
- 哈希密码
- 写入数据库(Prisma)
- 成功后 → toast 提示 + 路由跳转
🎯 教程模式的核心优势
| 优势 | 说明 |
|---|---|
| 类型安全贯穿全程 | 从 Schema → Server → Hook → UI 全链路 TS 类型推导 |
| 前后端验证双保险 | 前端zodResolver提升体验,后端schema.parse保证安全 |
| 关注点分离 | 每一层职责单一,便于测试、复用和团队协作 |
| 错误处理集中 | 通过executeAction封装通用错误逻辑(如事务回滚、日志) |
| 与现代生态集成 | React Hook Form + Zod + TanStack Query + Prisma + Server Actions |