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

在Remix中使用Zod实现多表单处理

在Remix中使用Zod处理多表单(基于_intent区分)

核心思路

先从提交的formData中提取_intent字段,根据这个值匹配对应的Zod Schema来解析表单数据,避免用统一Schema解析不同结构的表单导致验证错误。

步骤1:定义各表单的Zod Schema

为每个表单单独定义验证规则,确保只验证对应表单的字段:

import { z } from "zod";

// 登录表单Schema
const LoginSchema = z.object({
  _intent: z.literal("login"),
  email: z.string().email(),
  password: z.string().min(6),
});

// 注册表单Schema
const RegisterSchema = z.object({
  _intent: z.literal("register"),
  username: z.string().min(3),
  email: z.string().email(),
  password: z.string().min(8),
});

// 密码重置表单Schema
const ResetPasswordSchema = z.object({
  _intent: z.literal("reset-password"),
  email: z.string().email(),
});

步骤2:在Remix Action中处理表单提交

在路由的action函数里,先提取_intent,再用对应Schema解析,同时处理验证失败的情况:

import { ActionFunctionArgs, json } from "@remix-run/node";
import { z } from "zod";

// 示例业务逻辑函数,根据实际需求实现
async function handleLogin(data: z.infer<typeof LoginSchema>) {
  // 登录逻辑
}
async function handleRegister(data: z.infer<typeof RegisterSchema>) {
  // 注册逻辑
}
async function handleResetPassword(data: z.infer<typeof ResetPasswordSchema>) {
  // 密码重置逻辑
}

export async function action({ request }: ActionFunctionArgs) {
  const formData = await request.formData();
  const intent = formData.get("_intent") as string;

  let parsedData;
  try {
    switch (intent) {
      case "login":
        parsedData = LoginSchema.parse(Object.fromEntries(formData));
        await handleLogin(parsedData);
        break;
      case "register":
        parsedData = RegisterSchema.parse(Object.fromEntries(formData));
        await handleRegister(parsedData);
        break;
      case "reset-password":
        parsedData = ResetPasswordSchema.parse(Object.fromEntries(formData));
        await handleResetPassword(parsedData);
        break;
      default:
        throw new Error("无效的表单意图");
    }
  } catch (error) {
    if (error instanceof z.ZodError) {
      return json(
        { errors: error.flatten().fieldErrors, intent },
        { status: 400 }
      );
    }
    return json({ error: "处理失败" }, { status: 500 });
  }

  return json({ success: true });
}

步骤3:前端表单添加_intent隐藏字段

每个表单中加入隐藏的_intent字段,明确当前提交的表单类型:

import { Form, useActionData } from "@remix-run/react";

export default function AuthPage() {
  const actionData = useActionData<typeof action>();

  return (
    <div className="auth-container">
      {/* 登录表单 */}
      <Form method="post">
        <input type="hidden" name="_intent" value="login" />
        <div>
          <label>邮箱</label>
          <input type="email" name="email" defaultValue={actionData?.intent === "login" ? actionData.data?.email : ""} />
          {actionData?.intent === "login" && actionData.errors?.email && (
            <p className="error">{actionData.errors.email[0]}</p>
          )}
        </div>
        <div>
          <label>密码</label>
          <input type="password" name="password" />
          {actionData?.intent === "login" && actionData.errors?.password && (
            <p className="error">{actionData.errors.password[0]}</p>
          )}
        </div>
        <button type="submit">登录</button>
      </Form>

      {/* 注册表单 */}
      <Form method="post">
        <input type="hidden" name="_intent" value="register" />
        <div>
          <label>用户名</label>
          <input type="text" name="username" defaultValue={actionData?.intent === "register" ? actionData.data?.username : ""} />
          {actionData?.intent === "register" && actionData.errors?.username && (
            <p className="error">{actionData.errors.username[0]}</p>
          )}
        </div>
        <div>
          <label>邮箱</label>
          <input type="email" name="email" defaultValue={actionData?.intent === "register" ? actionData.data?.email : ""} />
          {actionData?.intent === "register" && actionData.errors?.email && (
            <p className="error">{actionData.errors.email[0]}</p>
          )}
        </div>
        <div>
          <label>密码</label>
          <input type="password" name="password" />
          {actionData?.intent === "register" && actionData.errors?.password && (
            <p className="error">{actionData.errors.password[0]}</p>
          )}
        </div>
        <button type="submit">注册</button>
      </Form>
    </div>
  );
}

关键注意点

  • 每个Schema都包含_intent的字面量验证,确保解析的是对应表单的数据
  • 解析前将formData转为普通对象(Object.fromEntries(formData)),方便Zod处理
  • 验证错误返回时带上intent,前端可根据该值仅显示对应表单的错误提示

内容的提问来源于stack exchange,提问作者nadiTime

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.06 05:20:08