如何让提交按钮在所有Yup字段验证通过时即时启用(无需失焦)
解决React Hook Form提交按钮无法实时根据验证状态启用/禁用的问题
问题截图

问题描述
我编写了一个重置密码的React组件,使用React Hook Form结合Yup做表单验证,希望提交按钮在所有输入字段满足验证规则前保持禁用,但当前存在两个问题:
- 输入符合要求的密码后,按钮仍处于禁用状态,仅点击输入框外部才会启用;
- 即使密码不满足规则,点击输入框外部也会错误激活按钮。
期望实现:输入完最后一个符合验证规则的字符时,按钮立即启用。
组件代码
import { FC, FormEventHandler } from 'react'; import { useForm } from 'react-hook-form'; import { yupResolver } from '@hookform/resolvers/yup'; import * as yup from 'yup'; import { useResetPasswordMutation } from 'redux/api/authenticationApi'; import InputField from 'components/ui/atoms/InputField'; import NewPasswordField from 'components/ui/atoms/NewPasswordField'; import { KeysIcon } from 'components/ui/atoms/Icon'; import { Link } from 'react-router-dom'; interface UpdatePasswordFormProps { token: string; onSent: () => void; } const schema = yup .object({ password: yup .string() .label('Password') .required() .min(8) .test('isValidPass', ' is not valid', (value: any, context) => { const hasUpperCase = /[A-Z]/.test(value); const hasLowerCase = /[a-z]/.test(value); const hasNumber = /[0-9]/.test(value); const hasSymbole = /[!@#%&]/.test(value); let validConditions = 0; const numberOfMustBeValidConditions = 3; const conditions = [hasLowerCase, hasUpperCase, hasNumber, hasSymbole]; conditions.forEach((condition) => condition ? validConditions++ : null ); if (validConditions >= numberOfMustBeValidConditions) { return true; } return false; }), password_confirmation: yup .string() .label('Password confirmation') .required() .oneOf([yup.ref('password'), null], 'Passwords must match') }) .required(); interface Inputs extends yup.InferType<typeof schema> { token: string; base: null; } const UpdatePasswordForm: FC<UpdatePasswordFormProps> = ({ token, onSent }) => { const { register, handleSubmit, setError, formState } = useForm<Inputs>({ mode: 'onTouched', resolver: yupResolver(schema) }); const [resetPassword] = useResetPasswordMutation(); register('base', { disabled: true }); const onSubmit: FormEventHandler<HTMLFormElement> = async (event) => { try { await handleSubmit(async (data) => { await resetPassword({ ...data, token }).unwrap(); onSent(); })(event); } catch (error: any) { if (error.data && 'errors' in error.data) { const errors = {} as { [key: string]: string[] }; error.data.errors.forEach( ({ title, details, source }: { title: string; details: string[]; source?: { pointer: string }; }) => { const pointer = source?.pointer && Object.keys(schema.fields).includes(source.pointer) ? source.pointer : 'base'; errors[pointer] = errors[pointer] ?? []; if (details && details.length > 0) { errors[pointer].push(...details); } else { errors[pointer].push(title); } } ); Object.entries(errors).forEach(([pointer, errors]) => { if (['password', 'password_confirmation', 'base'].includes(pointer)) { setError( pointer as any, { type: 'custom', message: errors.join('. ') }, { shouldFocus: true } ); } }); } else { const errorMessage = typeof error.data == 'string' ? error.data : 'Something went wrong.'; setError( 'base', { type: 'custom', message: errorMessage }, { shouldFocus: true } ); } } }; return ( <div className="flex w-82 flex-col text-inpay-black-500"> <KeysIcon width="40" height="40" className="mb-4 flex h-10 w-10 self-center stroke-inpay-black-500 stroke-1.5" /> <h1 className="mb-5 text-center text-[2rem] font-medium tracking-[0.01rem]"> Create new password </h1> <form className="flex flex-col" onSubmit={onSubmit}> <NewPasswordField {...register('password')} label="Enter new password" errorMessage={formState.errors.password?.message} /> <InputField {...register('password_confirmation')} type="password" label="Repeat password" className="mt-12" errorMessage={formState.errors.password_confirmation?.message} /> <button className="mt-10 h-12 rounded-lg bg-inpay-green-700 p-2 text-base font-medium text-white disabled:bg-inpay-gray-700" type="submit" value="Submit" disabled={ formState.isSubmitSuccessful || !formState.isValid || Object.keys(Error).length > 0 } > Create new password </button> {formState.errors.base && ( <div role="alert" className="mt-2 rounded-lg bg-rose-100 p-3 text-sm text-rose-600" > {formState.errors.base.message} </div> )} </form> <div className="mt-24 text-center"> <Link to="/login" className="font-medium text-inpay-blue-600 underline"> Back to log in </Link> </div> </div> ); }; export default UpdatePasswordForm;
解决方案
1. 修改React Hook Form的验证模式
当前使用的mode: 'onTouched'只会在字段失去焦点时触发验证,导致formState.isValid无法实时更新。将模式改为onChange,这样每次输入内容变化都会立即验证:
const { register, handleSubmit, setError, formState } = useForm<Inputs>({ mode: 'onChange', // 替换原来的onTouched resolver: yupResolver(schema) });
2. 修复按钮禁用条件的错误
按钮的disabled属性里有一个错误的判断Object.keys(Error).length > 0——Error是JavaScript全局对象,它本身包含大量属性,这个条件永远为true,导致按钮一直被禁用。需要移除这个错误条件,保留合理的判断:
<button className="mt-10 h-12 rounded-lg bg-inpay-green-700 p-2 text-base font-medium text-white disabled:bg-inpay-gray-700" type="submit" value="Submit" disabled={ formState.isSubmitSuccessful || !formState.isValid } > Create new password </button>
3. 确保自定义输入组件正确传递事件
检查InputField和NewPasswordField组件,确保它们正确传递了register返回的onChange、onBlur等事件属性,这样React Hook Form才能实时获取输入值并更新验证状态。示例自定义组件实现:
const InputField = ({ label, errorMessage, ...props }) => { return ( <div> <label>{label}</label> <input {...props} /> {errorMessage && <span className="error">{errorMessage}</span>} </div> ); };
完成以上修改后,输入框内容变化时会实时触发验证,formState.isValid会同步更新,按钮就能在所有字段满足规则时立即启用,不符合规则时保持禁用。
内容的提问来源于stack exchange,提问作者user19625670
相关产品推荐
相关产品推荐

