Comprehensive validation system supporting both synchronous and asynchronous validation at form and field levels with performance optimization and cross-field validation capabilities.
Form-wide validation that validates all form values together.
interface Config<FormValues, InitialFormValues> {
/** Form-level validation function */
validate?: (values: FormValues) => ValidationErrors | Promise<ValidationErrors>;
/** Whether to validate fields on blur event (default: false) */
validateOnBlur?: boolean;
}
type ValidationErrors = AnyObject | undefined;
type AnyObject = { [key: string]: any };Usage Examples:
import { createForm, FORM_ERROR } from "final-form";
// Synchronous form validation
const form = createForm({
onSubmit: (values) => console.log(values),
validate: (values) => {
const errors = {};
// Required field validation
if (!values.email) {
errors.email = 'Email is required';
}
// Email format validation
if (values.email && !/\S+@\S+\.\S+/.test(values.email)) {
errors.email = 'Invalid email format';
}
// Cross-field validation
if (values.password !== values.confirmPassword) {
errors.confirmPassword = 'Passwords must match';
}
// Form-level error
if (values.username === 'admin' && values.password === 'admin') {
errors[FORM_ERROR] = 'Invalid credentials combination';
}
return errors;
}
});
// Asynchronous form validation
const asyncForm = createForm({
onSubmit: async (values) => await submitUser(values),
validate: async (values) => {
const errors = {};
// Async username availability check
if (values.username) {
try {
const isAvailable = await checkUsernameAvailability(values.username);
if (!isAvailable) {
errors.username = 'Username is already taken';
}
} catch (error) {
errors.username = 'Unable to verify username availability';
}
}
// Async email validation
if (values.email) {
const isValidDomain = await validateEmailDomain(values.email);
if (!isValidDomain) {
errors.email = 'Email domain is not allowed';
}
}
return errors;
}
});
// Validation on blur
const blurValidationForm = createForm({
onSubmit: (values) => console.log(values),
validateOnBlur: true, // Validate fields when they lose focus
validate: (values) => {
const errors = {};
if (values.name && values.name.length < 2) {
errors.name = 'Name must be at least 2 characters';
}
return errors;
}
});Individual field validation with support for cross-field dependencies.
interface FieldConfig<FieldValue = any> {
/** Function that returns a validator for this field */
getValidator?: GetFieldValidator<FieldValue>;
/** Names of other fields to validate when this field changes */
validateFields?: string[];
/** Whether field validation is asynchronous */
async?: boolean;
}
type GetFieldValidator<FieldValue = any> = () => FieldValidator<FieldValue> | undefined;
type FieldValidator<FieldValue = any> = (
value: FieldValue,
allValues: object,
meta?: FieldState<FieldValue>
) => any | Promise<any>;Usage Examples:
// Basic field validation
form.registerField(
'email',
(fieldState) => updateEmailField(fieldState),
{ value: true, error: true },
{
getValidator: () => (value, allValues) => {
if (!value) return 'Email is required';
if (!/\S+@\S+\.\S+/.test(value)) return 'Invalid email format';
// Access other form values for cross-field validation
if (allValues.confirmEmail && value !== allValues.confirmEmail) {
return 'Email addresses must match';
}
}
}
);
// Async field validation
form.registerField(
'username',
(fieldState) => updateUsernameField(fieldState),
{ value: true, error: true, validating: true },
{
async: true,
getValidator: () => async (value) => {
if (!value) return 'Username is required';
if (value.length < 3) return 'Username must be at least 3 characters';
// Async validation
try {
const isAvailable = await checkUsernameAvailability(value);
if (!isAvailable) {
return 'Username is already taken';
}
} catch (error) {
return 'Unable to validate username';
}
}
}
);
// Cross-field validation with validateFields
form.registerField(
'password',
(fieldState) => updatePasswordField(fieldState),
{ value: true, error: true },
{
validateFields: ['confirmPassword'], // Re-validate confirmPassword when password changes
getValidator: () => (value) => {
if (!value) return 'Password is required';
if (value.length < 8) return 'Password must be at least 8 characters';
if (!/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(value)) {
return 'Password must contain uppercase, lowercase, and number';
}
}
}
);
form.registerField(
'confirmPassword',
(fieldState) => updateConfirmPasswordField(fieldState),
{ value: true, error: true },
{
getValidator: () => (value, allValues) => {
if (!value) return 'Please confirm your password';
if (value !== allValues.password) {
return 'Passwords must match';
}
}
}
);Methods for controlling validation behavior programmatically.
interface FormApi<FormValues, InitialFormValues> {
/** Check if validation is currently paused */
isValidationPaused(): boolean;
/** Pause all validation */
pauseValidation(): void;
/** Resume validation */
resumeValidation(): void;
}Usage Examples:
// Pause validation during bulk updates
form.pauseValidation();
form.batch(() => {
form.change('field1', 'value1');
form.change('field2', 'value2');
form.change('field3', 'value3');
});
form.resumeValidation(); // Validation will run once when resumed
// Conditional validation pausing
if (isImportingData) {
form.pauseValidation();
await importFormData();
form.resumeValidation();
}
// Check validation state
if (form.isValidationPaused()) {
console.log('Validation is currently paused');
}Common patterns for handling validation errors.
// Special error constants
const FORM_ERROR: string;
const ARRAY_ERROR: string;Error Handling Examples:
import { createForm, FORM_ERROR, ARRAY_ERROR } from "final-form";
// Form-level errors
const form = createForm({
onSubmit: async (values) => {
try {
await submitData(values);
} catch (error) {
// Return form-level error
return { [FORM_ERROR]: 'Submission failed. Please try again.' };
}
},
validate: (values) => {
const errors = {};
// Field-specific errors
if (!values.name) {
errors.name = 'Name is required';
}
// Form-level error for business logic
if (values.age < 18 && values.requiresParentalConsent !== true) {
errors[FORM_ERROR] = 'Parental consent required for users under 18';
}
return errors;
}
});
// Array field errors
const arrayForm = createForm({
onSubmit: (values) => console.log(values),
validate: (values) => {
const errors = {};
if (values.items && Array.isArray(values.items)) {
const itemErrors = values.items.map((item, index) => {
const itemError = {};
if (!item.name) {
itemError.name = 'Item name is required';
}
if (!item.price || item.price <= 0) {
itemError.price = 'Price must be greater than 0';
}
return Object.keys(itemError).length > 0 ? itemError : undefined;
});
// Check if array has any errors
if (itemErrors.some(error => error)) {
errors.items = itemErrors;
}
// Array-level error
if (values.items.length === 0) {
errors.items = { [ARRAY_ERROR]: 'At least one item is required' };
}
}
return errors;
}
});
// Error display patterns
form.subscribe(
(formState) => {
// Display form-level errors
if (formState.error) {
showFormError(formState.error);
}
// Display field-level errors
if (formState.errors) {
Object.keys(formState.errors).forEach(fieldName => {
if (fieldName !== FORM_ERROR) {
showFieldError(fieldName, formState.errors[fieldName]);
}
});
}
},
{ error: true, errors: true }
);Best practices for optimizing validation performance.
// ✅ Good: Memoized validators
const createEmailValidator = () => {
const emailRegex = /\S+@\S+\.\S+/;
return (value) => {
if (!value) return 'Email is required';
if (!emailRegex.test(value)) return 'Invalid email format';
};
};
form.registerField(
'email',
(fieldState) => updateEmailField(fieldState),
{ value: true, error: true },
{ getValidator: createEmailValidator }
);
// ✅ Good: Debounced async validation
const createAsyncValidator = () => {
let timeoutId;
return async (value) => {
if (!value) return 'Username is required';
// Debounce async validation
return new Promise((resolve) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(async () => {
try {
const isAvailable = await checkUsernameAvailability(value);
resolve(isAvailable ? undefined : 'Username taken');
} catch (error) {
resolve('Validation error');
}
}, 300);
});
};
};
// ❌ Bad: Creating new validators on every render
form.registerField(
'email',
(fieldState) => updateEmailField(fieldState),
{ value: true, error: true },
{
getValidator: () => (value) => { // New function every time
if (!value) return 'Email is required';
return /\S+@\S+\.\S+/.test(value) ? undefined : 'Invalid email';
}
}
);
// ✅ Good: Conditional validation
const createConditionalValidator = (condition) => {
return (value, allValues) => {
if (!condition(allValues)) {
return undefined; // Skip validation if condition not met
}
if (!value) return 'This field is required';
// ... other validation logic
};
};
form.registerField(
'conditionalField',
(fieldState) => updateField(fieldState),
{ value: true, error: true },
{
getValidator: () => createConditionalValidator(
(values) => values.enableConditionalField === true
)
}
);