Validation should run as soon as the user interacts with the field (e.g. on change or blur).
Error messages should show immediately when a user enters invalid data into a field (i.e. not just after clicking "Next").
How can I get real-time validation state just for the current step’s fields, and make the “Next” button reactive based on that?
Set all
mode
for your form. Read the API. Example:
const form = useForm({
resolver: zodResolver(resourceNoteFormSchema),
defaultValues: { note: '' },
mode: 'all'
});
The "Next" button should be disabled by default, and should only become active when all fields in the current step are valid.
Use getFieldState API to get field states for step + useWatch API to subscribe on step updates. Example:
const useStepValidity = (form, stepFields) => {
// Watch all current step fields so we re-render when they change
const watch = useWatch({ control: form.control, name: stepFields.map(({ name }) => name) });
return useMemo(() => {
return stepFields.every((stepField) => {
const { isDirty, invalid } = form.getFieldState(stepField.name);
return (isDirty || stepField.isOptional) && !invalid;
});
}, [form, stepFields, watch]);
};
Full Example :
const useStepValidity = (form, stepFields) => {
// Watch all current step fields so we re-render when they change
const watch = useWatch({ control: form.control, name: stepFields.map(({ name }) => name) });
return useMemo(() => {
return stepFields.every((stepField) => {
const { isDirty, invalid } = form.getFieldState(stepField.name);
return (isDirty || stepField.isOptional) && !invalid;
});
}, [form, stepFields, watch]);
};
const TestForm = () => {
const form = useForm({
mode: 'onChange',
defaultValues: {
firstName: '',
lastName: '',
email: ''
}
});
const { register } = form;
const isFirstStepReady = useStepValidity(form, [
{ name: 'firstName' },
{ name: 'lastName', isOptional: true }
]);
const isSecondStepReady = useStepValidity(form, [{ name: 'email' }]);
return (
<form>
<div>
<label>First Name</label>
<input {...register('firstName', { required: true })} />
</div>
<div>
<label>Last Name</label>
<input {...register('lastName')} />
</div>
<div>
<label>Email</label>
<input {...register('email', { required: true })} />
</div>
<p>First Step Ready: {isFirstStepReady ? 'Yes' : 'No'}</p>
<p>Second Step Ready: {isSecondStepReady ? 'Yes' : 'No'}</p>
</form>
);
};
NOTE: getFieldState
you should handle fields that optional but have some other validation rules (is it covered in the example).