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

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实例的验证逻辑:

  1. 空字段提交时,验证规则生效,onSubmit不会被调用,同时显示错误提示;
  2. 输入有效内容后提交,验证通过,onSubmit正常触发。

内容的提问来源于stack exchange,提问作者arista

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 01:00:00