如何在React/Next中用Zod和react-hook-form复刻Svelte表单校验日志?
在React/Next中复刻SvelteKit Zod表单校验功能
问题背景
跟着Svelte的Zod校验教程学习,但没有Svelte使用经验,想在简单的React/Next应用中复刻示例代码的功能。不清楚示例中async函数的({request})对应什么参数,同时疑惑该校验逻辑应该放在useEffect中还是表单提交时调用?
原SvelteKit示例代码:
export const actions = { default: async ({ request }) => { const formData = Object.fromEntries(await request.formData()); console.log('Form Data:', formData); try { const result = registerSchema.parse(formData); console.log('SUCCESS'); console.log(result); } catch (err) { const { fieldErrors: errors } = err.flatten(); const { password, passwordConfirm, ...rest } = formData; return { data: rest, errors }; } };
核心问题解答
1. ({request}) 对应React/Next中的什么?
SvelteKit里的actions是服务端处理表单提交的专属逻辑,request是包含表单提交数据的请求对象。在React/Next中,它对应的是表单提交事件中提取的FormData对象——也就是用户提交表单时,从<form>元素里拿到的所有输入数据。
2. 校验逻辑该放哪里?
绝对不要放在useEffect里!useEffect是用来处理页面渲染后的副作用(比如数据请求),而表单校验属于用户交互触发的动作,必须绑定在表单提交事件上执行,或者在Next.js中用Server Action在服务端处理。
具体实现示例
先定义Zod校验Schema(和原示例一致)
import { z } from 'zod'; const registerSchema = z.object({ username: z.string().min(3, '用户名至少3个字符'), email: z.string().email('邮箱格式不正确'), password: z.string().min(6, '密码至少6个字符'), passwordConfirm: z.string().min(6).refine((val, ctx) => val === ctx.data.password, { message: '两次密码不一致' }) });
1. React客户端实现(纯前端校验)
import { useState } from 'react'; import { z } from 'zod'; // 上面的registerSchema定义放在这里 export default function RegisterForm() { const [formData, setFormData] = useState({ username: '', email: '', password: '', passwordConfirm: '' }); const [errors, setErrors] = useState({}); const handleSubmit = async (e) => { e.preventDefault(); // 阻止默认表单提交行为 try { // 用Zod校验表单数据 const validatedData = registerSchema.parse(formData); console.log('校验成功:', validatedData); // 这里可以写提交到后端的逻辑,比如fetch请求 } catch (err) { // 提取字段错误,和原示例逻辑一致 const { fieldErrors } = err.flatten(); setErrors(fieldErrors || {}); // 清除密码字段(和原示例一致) setFormData(prev => ({ ...prev, password: '', passwordConfirm: '' })); } }; const handleInputChange = (e) => { const { name, value } = e.target; setFormData(prev => ({ ...prev, [name]: value })); // 输入时清除对应字段的错误提示 if (errors[name]) { setErrors(prev => ({ ...prev, [name]: undefined })); } }; return ( <form onSubmit={handleSubmit}> <div> <label>用户名</label> <input type="text" name="username" value={formData.username} onChange={handleInputChange} /> {errors.username && <span style={{color: 'red'}}>{errors.username[0]}</span>} </div> <div> <label>邮箱</label> <input type="email" name="email" value={formData.email} onChange={handleInputChange} /> {errors.email && <span style={{color: 'red'}}>{errors.email[0]}</span>} </div> <div> <label>密码</label> <input type="password" name="password" value={formData.password} onChange={handleInputChange} /> {errors.password && <span style={{color: 'red'}}>{errors.password[0]}</span>} </div> <div> <label>确认密码</label> <input type="password" name="passwordConfirm" value={formData.passwordConfirm} onChange={handleInputChange} /> {errors.passwordConfirm && <span style={{color: 'red'}}>{errors.passwordConfirm[0]}</span>} </div> <button type="submit">注册</button> </form> ); }
2. Next.js App Router 服务端校验(用Server Action)
如果想和SvelteKit一样在服务端处理校验(更安全,避免前端校验被绕过),可以用Next的Server Action:
// app/register/page.jsx 'use client'; import { useFormState } from 'react-dom'; import { registerAction } from './actions'; export default function RegisterPage() { // useFormState用来接收服务端返回的错误和保留的表单数据 const [state, formAction] = useFormState(registerAction, { data: {}, errors: {} }); return ( <form action={formAction}> <div> <label>用户名</label> <input type="text" name="username" defaultValue={state.data.username} /> {state.errors.username && <span style={{color: 'red'}}>{state.errors.username[0]}</span>} </div> <div> <label>邮箱</label> <input type="email" name="email" defaultValue={state.data.email} /> {state.errors.email && <span style={{color: 'red'}}>{state.errors.email[0]}</span>} </div> <div> <label>密码</label> <input type="password" name="password" /> {state.errors.password && <span style={{color: 'red'}}>{state.errors.password[0]}</span>} </div> <div> <label>确认密码</label> <input type="password" name="passwordConfirm" /> {state.errors.passwordConfirm && <span style={{color: 'red'}}>{state.errors.passwordConfirm[0]}</span>} </div> <button type="submit">注册</button> </form> ); }
然后创建服务端Action文件:
// app/register/actions.js 'use server'; import { z } from 'zod'; const registerSchema = z.object({ username: z.string().min(3, '用户名至少3个字符'), email: z.string().email('邮箱格式不正确'), password: z.string().min(6, '密码至少6个字符'), passwordConfirm: z.string().min(6).refine((val, ctx) => { // 从上下文获取原密码字段进行对比 const password = ctx.data.password; return val === password; }, { message: '两次密码不一致' }) }); export async function registerAction(prevState, formData) { // 把FormData转成对象,和SvelteKit的逻辑一致 const data = Object.fromEntries(formData); try { const validatedData = registerSchema.parse(data); console.log('校验成功:', validatedData); // 这里可以写数据库存储等逻辑 return { data: {}, errors: {} }; } catch (err) { const { fieldErrors: errors } = err.flatten(); // 过滤掉密码字段,避免回显 const { password, passwordConfirm, ...rest } = data; return { data: rest, errors }; } }
内容的提问来源于stack exchange,提问作者Damien Sullivan
相关产品推荐
相关产品推荐

