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

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>;

问题根源

  1. Zod 校验规则与登录场景不匹配
    登录表单仅需email和password,但当前FormSchema中username和confirmPassword被定义为z.string().nullable()——这意味着字段可以为null,但不能为undefined。而登录表单中这两个字段不存在,提交时会被解析为undefined,触发校验错误。

  2. 表单实例重复初始化导致状态丢失
    LoginForm组件每次渲染都会调用LoginService(),重新创建useForm实例,导致表单状态无法持久化。首次提交时,新实例未捕获到校验错误;第二次提交时才显示之前的错误,但校验未通过,表单无法提交。

  3. 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.05 10:54:59