React-Hook-Form表单提交异常:首次无响应,二次返回验证错误
表单提交异常问题分析与修复
问题现象
点击LoginForm组件中的SubmitBtn时:
- 首次提交无任何反应,
onInvalid函数返回空对象{}; - 第二次提交时,
onInvalid返回如下验证错误:
{ username: { message: 'Required', type: 'invalid_type', ref: undefined }, confirmPassword: { message: 'Required', type: 'invalid_type', ref: undefined } }
即便返回验证错误,表单仍无法提交,页面也无任何可见变化。
该表单原本功能正常,拆分为多组件后出现此问题。
相关代码
LoginForm 组件
import { FormControl } from '@chakra-ui/react' import SubmitBtn from '../SubmitBtn' import InputComponent from '../InputComponent'; import LoginService from '../../../services/loginService'; const LoginForm = () => { const { handleSubmit, onsubmit, register, isPending, errors, onInvalid } = LoginService() return ( <form onSubmit={handleSubmit(onsubmit, onInvalid)}> <FormControl isRequired mb='4'> <InputComponent label='Email Address' register={register} errorsType={errors} errorsMessage={errors.email?.message} placeholder='example@example.com' type='email' name='email' helper='please make sure to enter a valid email address' /> </FormControl> <FormControl isRequired mb='4'> <InputComponent label='Password' register={register} errorsType={errors} errorsMessage={errors.password?.message} placeholder='enter your password: ******' type='password' name='password' helper='please make sure to enter the correct password' /> </FormControl> <SubmitBtn loading={isPending} textloading='logging in' >Login</SubmitBtn> </form> ) } export default LoginForm
InputComponent 组件
import { FormHelperText, Input, FormLabel } from '@chakra-ui/react' import type { FieldErrors, UseFormRegister } from 'react-hook-form'; import { FormType } from '../../types/types'; type InputComponentPropsTypes = { register: UseFormRegister<FormType>, errorsType?: FieldErrors<FormType> | undefined, errorsMessage: string | undefined, type: React.HTMLInputTypeAttribute, name: keyof FormType label: string placeholder: string, helper: string } const InputComponent = ({ label, errorsType, errorsMessage, register, name, type, placeholder, helper }: InputComponentPropsTypes) => { return ( <> <FormLabel>{label}</FormLabel> <Input {...register(name)} type={type} placeholder={placeholder} focusBorderColor='teal.500' /> {errorsType && errorsType[name] ? <FormHelperText color='red'>{errorsMessage}</FormHelperText> : <FormHelperText>{helper}</FormHelperText> } </> ) } export default InputComponent
LoginService 逻辑
import { useToast } from '@chakra-ui/react' import { zodResolver } from '@hookform/resolvers/zod'; import { useForm } from 'react-hook-form' import axios from 'axios' import { useMutation } from '@tanstack/react-query' import { useNavigate } from 'react-router-dom' import { z } from 'zod' import { useStore } from '../utils/store'; import { FormSchema, FormType } from '../types/types'; import { API_URL } from '../constants'; const LoginService = () => { const setToken = useStore(store => store.setToken) const toast = useToast() const navigation = useNavigate() const { reset, register, handleSubmit, formState: { errors } } = useForm<FormType>({ resolver: zodResolver(FormSchema) }) const { mutateAsync: loginUser, isPending } = useMutation({ mutationKey: ['loginUser'], mutationFn: async (user: FormType) => { await axios.post(`${API_URL}auth/login`, user).then(res => { setToken(res.data.token) localStorage.setItem('token', res.data.token) }) reset() } }) const onInvalid = () => console.log(errors) const onsubmit = (user: FormType) => { console.log('clicked') loginUser(user, { onSuccess: () => { toast({ title: 'successfully logged in', position: 'top', status: 'success', isClosable: true, }) navigation('/home') }, onError: (error) => { if (error instanceof z.ZodError) { toast({ title: `${error.issues[0].message}`, position: 'top', status: 'error', isClosable: true, }) } else toast({ title: 'something went wrong', position: 'top', status: 'error', isClosable: true, }) } }) } return { onsubmit, onInvalid, register, handleSubmit, isPending, errors } } export default LoginService
类型定义
import { z } from "zod" export type AuthType = 'register' | 'login' export const FormSchema = z.object({ username: z.string().nullable(), email: z.string().email(), password: z.string().min(8, { message: 'Must be 8 or more characters long' }), confirmPassword: z.string().nullable() }).refine(data => data.password === data.confirmPassword, { message: 'confirmed password must match the password', path: ['confirmPassword'] }) export type FormType = z.infer<typeof FormSchema>;
问题根源
Zod 校验规则与登录场景不匹配
登录表单仅需email和password,但当前FormSchema中username和confirmPassword被定义为z.string().nullable()——这意味着字段可以为null,但不能为undefined。而登录表单中这两个字段不存在,提交时会被解析为undefined,触发校验错误。表单实例重复初始化导致状态丢失
LoginForm组件每次渲染都会调用LoginService(),重新创建useForm实例,导致表单状态无法持久化。首次提交时,新实例未捕获到校验错误;第二次提交时才显示之前的错误,但校验未通过,表单无法提交。onInvalid未获取实时校验错误
当前onInvalid直接打印组件初始化时的errors,并非校验失败后的实时错误,没有接收react-hook-form传递的错误参数。
修复方案
1. 拆分 Zod 校验规则,适配登录场景
创建单独的登录校验Schema,避免复用带注册字段的Schema:
// types.ts export const LoginFormSchema = z.object({ email: z.string().email(), password: z.string().min(8, { message: '密码长度不能少于8位' }) }) export type LoginFormType = z.infer<typeof LoginFormSchema>;
2. 调整LoginService为自定义Hook,确保实例稳定
将LoginService改为标准自定义Hook,同时设置表单默认值避免字段为undefined:
// LoginService.ts import { useToast } from '@chakra-ui/react' import { zodResolver } from '@hookform/resolvers/zod'; import { useForm } from 'react-hook-form' import axios from 'axios' import { useMutation } from '@tanstack/react-query' import { useNavigate } from 'react-router-dom' import { z } from 'zod' import { useStore } from '../utils/store'; import { LoginFormSchema, LoginFormType } from '../types/types'; import { API_URL } from '../constants'; export const useLoginService = () => { const setToken = useStore(store => store.setToken) const toast = useToast() const navigation = useNavigate() const { reset, register, handleSubmit, formState: { errors } } = useForm<LoginFormType>({ resolver: zodResolver(LoginFormSchema), defaultValues: { email: '', password: '' } }) const { mutateAsync: loginUser, isPending } = useMutation({ mutationKey: ['loginUser'], mutationFn: async (user: LoginFormType) => { const res = await axios.post(`${API_URL}auth/login`, user); setToken(res.data.token) localStorage.setItem('token', res.data.token) reset() } }) const onInvalid = (errors: any) => console.log(errors) const onsubmit = (user: LoginFormType) => { console.log('clicked') loginUser(user, { onSuccess: () => { toast({ title: '登录成功', position: 'top', status: 'success', isClosable: true, }) navigation('/home') }, onError: (error) => { if (error instanceof z.ZodError) { toast({ title: error.issues[0].message, position: 'top', status: 'error', isClosable: true, }) } else { toast({ title: '登录失败,请重试', position: 'top', status: 'error', isClosable: true, }) } } }) } return { onsubmit, onInvalid, register, handleSubmit, isPending, errors } }
3. 更新组件适配新的类型与Hook
修改LoginForm使用新的Hook:
// LoginForm.tsx import { FormControl } from '@chakra-ui/react' import SubmitBtn from '../SubmitBtn' import InputComponent from '../InputComponent'; import { useLoginService } from '../../../services/loginService'; const LoginForm = () => { const { handleSubmit, onsubmit, register, isPending, errors, onInvalid } = useLoginService() return ( <form onSubmit={handleSubmit(onsubmit, onInvalid)}> <FormControl isRequired mb='4'> <InputComponent label='邮箱地址' register={register} errorsType={errors} errorsMessage={errors.email?.message} placeholder='example@example.com' type='email' name='email' helper='请输入有效的邮箱地址' /> </FormControl> <FormControl isRequired mb='4'> <InputComponent label='密码' register={register} errorsType={errors} errorsMessage={errors.password?.message} placeholder='请输入密码:******' type='password' name='password' helper='密码长度不能少于8位' /> </FormControl> <SubmitBtn loading={isPending} textloading='登录中' >登录</SubmitBtn> </form> ) } export default LoginForm
修改InputComponent的类型约束:
// InputComponent.tsx import { FormHelperText, Input, FormLabel } from '@chakra-ui/react' import type { FieldErrors, UseFormRegister } from 'react-hook-form'; import { LoginFormType } from '../../types/types'; type InputComponentPropsTypes = { register: UseFormRegister<LoginFormType>, errorsType?: FieldErrors<LoginFormType> | undefined, errorsMessage: string | undefined, type: React.HTMLInputTypeAttribute, name: keyof LoginFormType label: string placeholder: string, helper: string } const InputComponent = ({ label, errorsType, errorsMessage, register, name, type, placeholder, helper }: InputComponentPropsTypes) => { return ( <> <FormLabel>{label}</FormLabel> <Input {...register(name)} type={type} placeholder={placeholder} focusBorderColor='teal.500' /> {errorsType && errorsType[name] ? <FormHelperText color='red'>{errorsMessage}</FormHelperText> : <FormHelperText>{helper}</FormHelperText> } </> ) } export default InputComponent
内容的提问来源于stack exchange,提问作者Boughazi Akram
相关产品推荐
相关产品推荐

