全栈开发, 精选文章

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.tsclsx+twMergebcryptjs、安全转换函数
2. 数据验证层(Schema & Types)定义 Zod 表单 Schema + TypeScript 类型 + 默认值signUpSchema.tszodz.infer.refine()做复杂校验
3. 服务层(Server Action)在服务端执行业务逻辑(验证、数据库操作)mutations.ts"use server"executeAction封装错误/事务、Prisma
4. 数据获取层(Hooks)封装客户端数据交互逻辑(调用 Server Action + 处理响应)use-sign-up-mutations.tsuseMutation(TanStack Query)、useRoutertoast
5. UI 层(Components)渲染表单界面,绑定状态与提交逻辑sign-up-form.tsxreact-hook-form+zodResolverFormProvider、受控组件

🔍 流程执行顺序(从用户操作到数据落库)

  1. 用户填写表单react-hook-form 控制字段状态 + 实时 Zod 验证
  2. 点击提交form.handleSubmit(onSubmit) 触发
  3. 调用自定义 HookuseSignUp().mutate(data)
  4. Hook 调用 Server ActionsignUp(data)(走 use server
  5. Server Action
    • 用 Zod 再次校验(防绕过前端)
    • 哈希密码
    • 写入数据库(Prisma)
  6. 成功后 → toast 提示 + 路由跳转

🎯 教程模式的核心优势

优势说明
类型安全贯穿全程从 Schema → Server → Hook → UI 全链路 TS 类型推导
前后端验证双保险前端zodResolver提升体验,后端schema.parse保证安全
关注点分离每一层职责单一,便于测试、复用和团队协作
错误处理集中通过executeAction封装通用错误逻辑(如事务回滚、日志)
与现代生态集成React Hook Form + Zod + TanStack Query + Prisma + Server Actions