我在项目中使用 React Hook Form 来处理表单验证。我有一个自定义钩子 useFormField ,它与 React Hook Form 中的 useController 集成。但是,我遇到了一个问题,即使不满足验证规则,也不会阻止表单提交。
相关代码如下:
自定义挂钩(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');
// Submit with empty field
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');
});
// Fill in the field and submit again
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();
});
});
尽管定义了验证规则,当表单提交为空时,onSubmit 函数仍然被调用。预期的行为是应阻止表单提交并显示验证错误。
什么可能导致即使验证失败,表单提交仍继续进行?如何确保在不满足验证规则时正确阻止表单提交?
在 TestComponent 中,您再次调用 useForm,这会创建两个单独的表单定义。自定义挂钩中的控制值来自上下文,而其他值来自 TestComponent 中新定义的表单。您应该改用 Wrapper 组件中的上下文。
function TestComponent({
name,
defaultValue,
rules,
onSubmit = (d) => {},
}: {
name: string;
defaultValue?: any;
rules?: any;
onSubmit?: (data: any) => void;
}) {
const methods = useFormContext<FieldValues>(); // this line
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 && <div data-testid="error">{error.message}</div>}
<div data-testid="touched">{isTouched ? 'touched' : 'untouched'}</div>
<div data-testid="dirty">{isDirty ? 'dirty' : 'clean'}</div>
<div data-testid="submitting">
{isSubmitting ? 'submitting' : 'not submitting'}
</div>
<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>;
}
确保在 TestComponent 中使用 useFormContext 来访问正确的表单上下文,确保控件和其他状态值在组件之间保持一致。