React Hook Form自定义useController钩子验证无法阻止表单提交
问题:React Hook Form自定义useFormField后,验证失败时表单仍能提交
我在项目中使用React Hook Form处理表单验证,自定义了集成useController的useFormField钩子,但遇到验证规则未满足时表单提交仍无法被阻止的问题:提交空字段时onSubmit函数仍会被调用,预期行为是表单提交被阻止并显示验证错误。
相关代码如下:
自定义钩子(useFormField)
import { useController, useFormContext, RegisterOptions, FieldValues, FieldPath, UseControllerProps } from 'react-hook-form'; type UseFormFieldProps<TFieldValues extends FieldValues> = { name: FieldPath<TFieldValues>; defaultValue?: any; rules?: Omit<RegisterOptions<TFieldValues, FieldPath<TFieldValues>>, 'valueAsNumber' | 'valueAsDate' | 'setValueAs' | 'disabled'>; }; export function useFormField<TFieldValues extends FieldValues>({ name, defaultValue, rules }: UseFormFieldProps<TFieldValues>) { const { control } = useFormContext<TFieldValues>(); const { field, fieldState: { error, isTouched, isDirty }, formState: { isSubmitting } } = useController({ name, control, defaultValue, rules, } as UseControllerProps<TFieldValues>); return { ...field, error, isTouched, isDirty, isSubmitting, }; }
测试组件与测试用例
import React from 'react'; import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import { FormProvider, useForm } from 'react-hook-form'; import { useFormField } from '@/hooks/useFormField'; function TestComponent({ name, defaultValue, rules, onSubmit = (d) => {} }: { name: string, defaultValue?: any, rules?: any, onSubmit?: (data: any) => void }) { const methods = useForm({ mode: 'onChange' }); const { value, onChange, onBlur, error, isTouched, isDirty, isSubmitting } = useFormField({ name, defaultValue, rules }); return ( <form onSubmit={methods.handleSubmit(onSubmit)} data-testid="form"> <input type="text" value={value || ''} onChange={onChange} onBlur={onBlur} data-testid="input" /> {error && <span data-testid="error">{error.message}</span>} <span data-testid="touched">{isTouched ? 'touched' : 'untouched'}</span> <span data-testid="dirty">{isDirty ? 'dirty' : 'clean'}</span> <span data-testid="submitting">{isSubmitting ? 'submitting' : 'not submitting'}</span> <button type="submit" data-testid="submit">Submit</button> </form> ); } function Wrapper({ children }: { children: React.ReactNode }) { const methods = useForm({ mode: 'onChange' }); return <FormProvider {...methods}>{children}</FormProvider>; } // 测试用例 it('should apply validation rules on submit', async () => { const onSubmitMock = jest.fn(); render( <Wrapper> <TestComponent name="test" rules={{ required: 'This field is required' }} onSubmit={onSubmitMock} /> </Wrapper> ); const input = screen.getByTestId('input'); const submitButton = screen.getByTestId('submit'); // 提交空字段 fireEvent.click(submitButton); await waitFor(() => { expect(onSubmitMock).not.toHaveBeenCalled(); const errorElement = screen.queryByTestId('error'); expect(errorElement).not.toBeNull(); expect(errorElement).toHaveTextContent('This field is required'); }); // 填写内容后再次提交 fireEvent.change(input, { target: { value: 'valid input' } }); fireEvent.click(submitButton); await waitFor(() => { expect(onSubmitMock).toHaveBeenCalledWith(expect.objectContaining({ test: 'valid input' })); expect(screen.queryByTestId('error')).toBeNull(); }); });
问题原因
核心问题是存在两个独立的React Hook Form实例:
Wrapper组件中创建了一个form实例,并通过FormProvider提供上下文;TestComponent内部又创建了另一个form实例,并用它的handleSubmit处理表单提交;- 而
useFormField是从FormProvider的上下文获取control,关联的是Wrapper里的form实例,这就导致提交逻辑和字段验证逻辑分属两个不同的实例,提交时根本不会触发字段的验证规则。
修复方案
修改TestComponent,移除内部的useForm实例,从上下文获取handleSubmit,确保提交逻辑和字段验证使用同一个form实例:
function TestComponent({ name, defaultValue, rules, onSubmit = (d) => {} }: { name: string, defaultValue?: any, rules?: any, onSubmit?: (data: any) => void }) { // 从FormProvider上下文获取handleSubmit,不再自行创建useForm实例 const { handleSubmit } = useFormContext(); const { value, onChange, onBlur, error, isTouched, isDirty, isSubmitting } = useFormField({ name, defaultValue, rules }); return ( <form onSubmit={handleSubmit(onSubmit)} data-testid="form"> <input type="text" value={value || ''} onChange={onChange} onBlur={onBlur} data-testid="input" /> {error && <span data-testid="error">{error.message}</span>} <span data-testid="touched">{isTouched ? 'touched' : 'untouched'}</span> <span data-testid="dirty">{isDirty ? 'dirty' : 'clean'}</span> <span data-testid="submitting">{isSubmitting ? 'submitting' : 'not submitting'}</span> <button type="submit" data-testid="submit">Submit</button> </form> ); }
修复后效果
修改后,表单提交时会触发FormProvider实例的验证逻辑:
- 空字段提交时,验证规则生效,
onSubmit不会被调用,同时显示错误提示; - 输入有效内容后提交,验证通过,
onSubmit正常触发。
内容的提问来源于stack exchange,提问作者arista
相关产品推荐
相关产品推荐

