@matthew.ngo/reform
Version:
A flexible and powerful React form management library with advanced validation, state observation, and multi-group support
187 lines (160 loc) • 5.55 kB
text/typescript
import { useMemo, useRef, useState } from 'react';
import { ReformReturn } from '../../../types';
import {
DynamicSchemaConfig,
DynamicValidationRule,
ValidationContext,
ValidationRuleResult,
DynamicSchemaReturn,
} from './types';
import { FieldPath } from '../../form/form-groups';
import get from 'lodash/get';
import { useMemoizedCallback } from '../../../common/useMemoizedCallback';
/**
* Hook for managing dynamic schema validation
*
* @template T - The type of form data
* @param reform - The Reform hook return value
* @param config - Configuration for dynamic schema validation
* @returns Dynamic schema validation state and methods
*/
export const useDynamicValidation = <T extends Record<string, any>>(
reform: ReformReturn<T>,
config: DynamicSchemaConfig<T>
): DynamicSchemaReturn<T> => {
// Store reform reference to avoid unnecessary re-renders
const reformRef = useRef(reform);
// Memoize config to prevent unnecessary re-renders
const memoizedConfig = useMemo(
() => ({
validations: config.validations,
getContextData: config.getContextData,
}),
[
// Use JSON.stringify for complex objects to compare by value
JSON.stringify(config.validations),
config.getContextData,
]
);
const { validations, getContextData } = memoizedConfig;
// State for additional context data
const [contextData, setContextData] = useState<Record<string, any>>(
getContextData?.() || {}
);
// Create validation context for a specific group - memoized to prevent unnecessary re-renders
const createContext = useMemoizedCallback(
(groupIndex: number): ValidationContext<T> => {
const groups = reformRef.current.getGroups();
const currentGroup = groups[groupIndex] || { id: '', data: {} as T };
return {
groups,
currentGroup,
groupIndex,
contextData,
};
},
[contextData]
);
// Get applicable validation rules for a field - memoized to prevent unnecessary re-renders
const getFieldRules = useMemoizedCallback(
(groupIndex: number, field: FieldPath<T>): DynamicValidationRule<T>[] => {
const fieldValidation = validations.find(v => v.field === field);
if (!fieldValidation) return [];
const context = createContext(groupIndex);
const value = get(context.currentGroup.data, field);
// Filter rules based on conditions
return fieldValidation.rules.filter(rule => {
if (!rule.when) return true;
return rule.when(value, context);
});
},
[validations, createContext]
);
// Validate a specific field - memoized to prevent unnecessary re-renders
const validateField = useMemoizedCallback(
(
groupIndex: number,
field: FieldPath<T>,
value: any
): ValidationRuleResult => {
const context = createContext(groupIndex);
const rules = getFieldRules(groupIndex, field);
// No rules to apply
if (rules.length === 0) {
return { isValid: true };
}
// Apply each rule until one fails
for (const rule of rules) {
const result = rule.validate(value, context);
if (result === false || typeof result === 'string') {
let message: string;
if (typeof result === 'string') {
message = result;
} else if (typeof rule.message === 'function') {
message = rule.message(value, context);
} else {
message = rule.message || 'Validation failed';
}
return { isValid: false, message };
}
}
return { isValid: true };
},
[createContext, getFieldRules]
);
// Validate an entire group - memoized to prevent unnecessary re-renders
const validateGroup = useMemoizedCallback(
(groupIndex: number): boolean => {
const context = createContext(groupIndex);
const { currentGroup } = context;
// Get all fields that have validation rules
const fieldsToValidate = validations
.map(v => v.field)
.filter((field, index, self) => self.indexOf(field) === index);
// Validate each field
for (const field of fieldsToValidate) {
const value = get(currentGroup.data, field);
const result = validateField(groupIndex, field, value);
if (!result.isValid) {
return false;
}
}
return true;
},
[createContext, validations, validateField]
);
// Update context data - memoized to prevent unnecessary re-renders
const updateContext = useMemoizedCallback(
(newContextData: Record<string, any>) => {
setContextData(prev => ({
...prev,
...newContextData,
}));
},
[]
);
// Register validation with Reform
useMemo(() => {
// Update reform reference
reformRef.current = reform;
// Register custom validation for each field
validations.forEach(fieldValidation => {
const { field } = fieldValidation;
// FIXME
// reform.registerFieldValidator(field, (value, groupIndex) => {
// const result = validateField(groupIndex, field, value);
// return result.isValid ? true : result.message || 'Invalid value';
// });
});
}, [reform, validations, validateField]);
// Return memoized object to prevent unnecessary re-renders
return useMemo(
() => ({
validateField,
validateGroup,
getFieldRules,
updateContext,
}),
[validateField, validateGroup, getFieldRules, updateContext]
);
};