docs/form-validation.md
This document outlines the standardized form validation patterns and guidelines established in PR #39590 to ensure consistent user experience across Rocket.Chat forms.
The form validation standardization aims to:
mode: 'onSubmit')Forms should use mode: 'onSubmit' in react-hook-form to trigger initial validation only when the user attempts to submit the form.
Why: This approach improves accessibility by:
Example:
const {
control,
formState: { errors, isDirty, isSubmitting },
handleSubmit,
} = useForm<FormData>({
mode: 'onSubmit', // This can be omitted, `onSubmit` it's the default mode value
defaultValues: initialData,
});
After the first submit attempt, forms should revalidate fields intelligently:
reValidateMode: 'onChange'For most forms, use the default onChange revalidation to provide immediate feedback as users correct errors.
reValidateMode: 'onBlur' for Async ValidationFor forms with async validation (e.g., username availability, email uniqueness checks), explicitly set reValidateMode: 'onBlur' to avoid excessive API calls.
Example with async validation:
const {
control,
formState: { errors, isDirty },
handleSubmit,
} = useForm<FormData>({
reValidateMode: 'onBlur', // Avoid API calls on every keystroke
defaultValues: initialData,
});
useFormSubmitWithDirtyCheckUse the useFormSubmitWithDirtyCheck hook to provide user-friendly feedback when attempting to save unchanged forms.
Usually applicable on edit forms, where fields are already populated.
Purpose:
Signature:
Receives a callback as the first parameter (your submit handler), and an object as the second parameter containing isDirty and an optional noChangesMessage translation key, to be dispatched in the info toast.
Usage:
import { useFormSubmitWithDirtyCheck } from '/hooks/useFormSubmitWithDirtyCheck';
const handleSave = useFormSubmitWithDirtyCheck(
async (data: FormData) => {
try {
await saveData(data);
dispatchToastMessage({ type: 'success', message: t('Saved') });
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
}
},
{ isDirty }
);
// In JSX:
<form onSubmit={handleSubmit(handleSave)}>
When to use dirty-check:
This hook is recommended when the same form component is used for both creation (new) and editing existing data. The hook intelligently handles both scenarios:
import { useForm, Controller } from 'react-hook-form';
import { useFormSubmitWithDirtyCheck } from '../../../hooks/useFormSubmitWithDirtyCheck';
type FormData = {
name: string;
email: string;
};
const MyForm = ({ data, onSave }: FormProps) => {
const { t } = useTranslation();
const {
control,
formState: { errors, isDirty, isSubmitting },
handleSubmit,
} = useForm<FormData>({
defaultValues: data || {},
});
const handleFormSubmit = useFormSubmitWithDirtyCheck(
async (formData: FormData) => {
await onSave(formData);
},
{ isDirty }
);
return (
<form onSubmit={handleSubmit(handleFormSubmit)} id={formId}>
</form>
);
};
<Button
primary
type='submit'
form={formId}
loading={isSubmitting}
>
{t('Save')}
</Button>
Key Points:
loading={isSubmitting} to show loading state during submissionform={formId} attributeWhen updating an existing form to follow these guidelines:
mode to 'onSubmit' in useFormreValidateMode: 'onBlur' if form has async validationuseFormSubmitWithDirtyCheck (for create and edit forms)aria-describedby, aria-invalid, role='alert' when applicableloading={isSubmitting}, but never disabled// Bad - prevents discovery of validation requirements
<Button disabled={!isValid || !isDirty}>Save</Button>
// Good - accessible and provides feedback
<Button
type='submit'
disabled={existingId ? !isDirty : false}
loading={isSubmitting}
>
Save
</Button>
mode: 'onChange' for initial validation// Bad - shows errors immediately, poor UX
useForm({ mode: 'onChange' })
mode: 'onSubmit' for initial validation// Good - validates on submit, revalidates on change
useForm({ mode: 'onSubmit' })
reValidateMode: 'onChange' with async validation// Bad - causes API call on every keystroke
useForm({
mode: 'onSubmit',
// Uses default 'onChange' revalidation - too many API calls!
})
reValidateMode: 'onBlur' with async validation// Good - reduces API calls while maintaining feedback
useForm({
mode: 'onSubmit',
reValidateMode: 'onBlur',
})