SIGN IN SIGN UP
RocketChat / Rocket.Chat UNCLAIMED

The Secure CommsOS™ for mission-critical operations

45034 0 0 TypeScript
# Form Validation Guidelines
This document outlines the standardized form validation patterns and guidelines established in PR [#39590](https://github.com/RocketChat/Rocket.Chat/pull/39590) to ensure consistent user experience across Rocket.Chat forms.
## Overview
The form validation standardization aims to:
- **Improve accessibility** by keeping submit buttons enabled and letting validation run on submit
- **Provide consistent UX** with validation triggered on form submission and re-validation on field changes
- **Prevent unnecessary API calls** by using dirty-checks and appropriate revalidation modes
- **Enhance user feedback** with clear error messages and proper ARIA attributes
## Core Principles
### 1. Submit-First validation (`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:
- Keeping submit buttons enabled (allowing screen readers and keyboard users to discover validation requirements)
- Avoiding premature error messages that can confuse users
- Letting users complete the form at their own pace before seeing validation feedback
**Example:**
```tsx
const {
control,
formState: { errors, isDirty, isSubmitting },
handleSubmit,
} = useForm<FormData>({
mode: 'onSubmit', // This can be omitted, `onSubmit` it's the default mode value
defaultValues: initialData,
});
```
### 2. Smart revalidation strategy
After the first submit attempt, forms should revalidate fields intelligently:
#### Default: `reValidateMode: 'onChange'`
For most forms, use the default onChange revalidation to provide immediate feedback as users correct errors.
#### Exception: `reValidateMode: 'onBlur'` for Async Validation
For forms with **async validation** (e.g., username availability, email uniqueness checks), explicitly set `reValidateMode: 'onBlur'` to avoid excessive API calls.
**Example with async validation:**
```tsx
const {
control,
formState: { errors, isDirty },
handleSubmit,
} = useForm<FormData>({
reValidateMode: 'onBlur', // Avoid API calls on every keystroke
defaultValues: initialData,
});
```
### 3. Dirty-check with `useFormSubmitWithDirtyCheck`
Use 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:**
- Prevents unnecessary save operations on unchanged data
- Shows informative toast message: "No changes to save"
- Maintains accessibility by keeping buttons enabled
**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:**
```tsx
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:
-**Create mode** (no existing data): Allows submission without dirty check
-**Edit mode** (existing data): Shows "No changes to save" toast when form is unchanged
-**Unified component**: Simplifies logic by handling both create and edit in one place
## Form Implementation Patterns
### Basic Form Structure
```tsx
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 fields */}
</form>
);
};
```
## Button State Management
### Submit Button States
```tsx
<Button
primary
type='submit'
form={formId}
loading={isSubmitting}
>
{t('Save')}
</Button>
```
**Key Points:**
- Use `loading={isSubmitting}` to show loading state during submission
- Never disable the save button (keep enabled for a11y)
- Always connect button to form via `form={formId}` attribute
## Basic checklist
When updating an existing form to follow these guidelines:
- [ ] Use `mode` to `'onSubmit'` in `useForm`
- [ ] Add `reValidateMode: 'onBlur'` if form has async validation
- [ ] Wrap submit handler with `useFormSubmitWithDirtyCheck` (for create and edit forms)
- [ ] Add ARIA attributes: `aria-describedby`, `aria-invalid`, `role='alert'` when applicable
- [ ] Button states: `loading={isSubmitting}`, but never `disabled`
- [ ] Verify accessibility with screen reader testing
## Basic DOs and DON'Ts
### ❌ Don't: Disable buttons based on form validity
```tsx
// Bad - prevents discovery of validation requirements
<Button disabled={!isValid || !isDirty}>Save</Button>
```
### ✅ Do: Keep buttons enabled, let validation run on submit
```tsx
// Good - accessible and provides feedback
<Button
type='submit'
disabled={existingId ? !isDirty : false}
loading={isSubmitting}
>
Save
</Button>
```
### ❌ Don't: Use `mode: 'onChange'` for initial validation
```tsx
// Bad - shows errors immediately, poor UX
useForm({ mode: 'onChange' })
```
### ✅ Do: Use `mode: 'onSubmit'` for initial validation
```tsx
// Good - validates on submit, revalidates on change
useForm({ mode: 'onSubmit' })
```
### ❌ Don't: Use `reValidateMode: 'onChange'` with async validation
```tsx
// Bad - causes API call on every keystroke
useForm({
mode: 'onSubmit',
// Uses default 'onChange' revalidation - too many API calls!
})
```
### ✅ Do: Use `reValidateMode: 'onBlur'` with async validation
```tsx
// Good - reduces API calls while maintaining feedback
useForm({
mode: 'onSubmit',
reValidateMode: 'onBlur',
})
```
## Additional Resources
- [React Hook Form Documentation](https://react-hook-form.com/)
- [WCAG 2.1 Form Guidelines](https://www.w3.org/WAI/WCAG21/quickref/?showtechniques=332#error-identification)
- [PR #39590 - Form Validation Standardization](https://github.com/RocketChat/Rocket.Chat/pull/39590)