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

如何在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.24 01:07:54