在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
相关产品推荐
相关产品推荐

