The communications platform that puts data protection first.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
Rocket.Chat/docs/form-validation.md

6.7 KiB

Form Validation Guidelines

This document outlines the standardized form validation patterns and guidelines established in PR #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:

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:

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:

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

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

<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

// Bad - prevents discovery of validation requirements
<Button disabled={!isValid || !isDirty}>Save</Button>

Do: Keep buttons enabled, let validation run on submit

// Good - accessible and provides feedback
<Button 
  type='submit' 
  disabled={existingId ? !isDirty : false}
  loading={isSubmitting}
>
  Save
</Button>

Don't: Use mode: 'onChange' for initial validation

// Bad - shows errors immediately, poor UX
useForm({ mode: 'onChange' })

Do: Use mode: 'onSubmit' for initial validation

// Good - validates on submit, revalidates on change
useForm({ mode: 'onSubmit' })

Don't: Use reValidateMode: 'onChange' with async validation

// 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

// Good - reduces API calls while maintaining feedback
useForm({ 
  mode: 'onSubmit',
  reValidateMode: 'onBlur',
})

Additional Resources