12 KiB
1. ✅ Error Filtering Logic Inconsistency (RESOLVED)
Note: This issue has been resolved by implementing a type-based error system.
The filtering logic in ValidationCell.tsx previously relied on string matching, which was fragile:
// Old implementation (string-based matching)
const filteredErrors = React.useMemo(() => {
return !isEmpty(value)
? errors.filter(error => !error.message?.toLowerCase().includes('required'))
: errors;
}, [value, errors]);
// New implementation (type-based filtering)
const filteredErrors = React.useMemo(() => {
return !isEmpty(value)
? errors.filter(error => error.type !== ErrorType.Required)
: errors;
}, [value, errors]);
The solution implemented:
- Added an
ErrorTypeenum intypes.tsto standardize error categorization - Updated all error creation to include the appropriate error type
- Modified error filtering to use the type property instead of string matching
- Ensured consistent error handling across the application
Guidelines for future development:
- Always use the
ErrorTypeenum when creating errors - Never rely on string matching for error filtering
- Ensure all error objects include the
typeproperty - Use the appropriate error type for each validation rule:
ErrorType.Requiredfor required field validationsErrorType.Regexfor regex validationsErrorType.Uniquefor uniqueness validationsErrorType.Customfor custom validationsErrorType.Apifor API-based validations
2. Redundant Error Processing
The system processes errors in multiple places:
- In
ValidationCell.tsx, errors are filtered and processed again - In
useValidation.tsx, errors are already filtered once - In
ValidationContainer.tsx, errors are manipulated directly
This redundancy could lead to inconsistent behavior and makes the code harder to maintain.
3. Race Conditions in Async Validation
The UPC validation and other async validations could create race conditions:
- If a user types quickly, multiple validation requests might be in flight
- Later responses could overwrite more recent ones if they complete out of order
- The debouncing helps but doesn't fully solve this issue
4. Memory Leaks in Timeout Management
The validation timeouts are stored in refs:
const validationTimeoutsRef = useRef<Record<number, NodeJS.Timeout>>({});
While there is cleanup on unmount, if rows are added/removed dynamically, timeouts for deleted rows might not be properly cleared.
5. ✅ Inefficient Error Storage (RESOLVED)
Status: RESOLVED
Problem
Previously, validation errors were stored in multiple locations:
- In the
validationErrorsMap inuseValidationState - In the row data itself as
__errors
This redundancy caused several issues:
- Inconsistent error states between the two storage locations
- Increased memory usage by storing the same information twice
- Complex state management to keep both sources in sync
- Difficulty reasoning about where errors should be accessed from
Solution
We've implemented a unified error storage approach by:
- Making the
validationErrorsMap inuseValidationStatethe single source of truth for all validation errors - Removed the
__errorsproperty from row data - Updated all validation functions to interact with the central error store instead of modifying row data
- Modified UPC validation to use the central error store
- Updated all components to read errors from the
validationErrorsMap instead of row data
Key Changes
- Modified
dataMutations.tsto stop storing errors in row data - Updated the
Metatype to remove the__errorsproperty - Modified the
RowDatatype to remove the__errorsproperty - Updated the
useValidationhook to return errors separately from row data - Modified the
useAiValidationhook to work with the central error store - Updated the
useFiltershook to check for errors in thevalidationErrorsMap - Modified the
ValidationTableandValidationCellcomponents to read errors from thevalidationErrorsMap
Benefits
- Single Source of Truth: All validation errors are now stored in one place
- Reduced Memory Usage: No duplicate storage of error information
- Simplified State Management: Only one state to update when errors change
- Cleaner Data Structure: Row data no longer contains validation metadata
- More Maintainable Code: Clearer separation of concerns between data and validation
Future Improvements
While this refactoring addresses the core issue of inefficient error storage, there are still opportunities for further optimization:
- Redundant Error Processing: The validation process still performs some redundant calculations that could be optimized.
- Race Conditions: Async validation can lead to race conditions when multiple validations are triggered in quick succession.
- Memory Leaks: The timeout management for validation could be improved to prevent potential memory leaks.
- Tight Coupling: Components are still tightly coupled to the validation state structure.
- Error Prioritization: The system doesn't prioritize errors well, showing all errors at once rather than focusing on the most critical ones first.
Validation Flow
The validation process now works as follows:
-
Error Generation:
- Field-level validations generate errors based on validation rules
- Row-level hooks add custom validation errors
- Table-level validations (like uniqueness checks) add errors across rows
-
Error Storage:
- All errors are stored in the
validationErrorsMap inuseValidationState - The Map uses row indices as keys and objects of field errors as values
- All errors are stored in the
-
Error Display:
- The
ValidationTablecomponent checks thevalidationErrorsMap to highlight rows with errors - The
ValidationCellcomponent receives errors for specific fields from thevalidationErrorsMap - Errors are filtered in the UI to avoid showing "required" errors for fields with values
- The
This focused refactoring approach has successfully addressed a critical issue while keeping changes manageable and targeted.
6. Excessive Re-rendering
Despite optimization attempts, the system might still cause excessive re-renders:
- Each cell validation can trigger updates to the entire data structure
- The batch update system helps but still has limitations
- The memoization in
ValidationCellmight not catch all cases where re-rendering is unnecessary
7. Complex Error Merging Logic
When merging errors from different sources, the logic is complex and potentially error-prone:
// Merge field errors and row hook errors
const mergedErrors: Record<string, InfoWithSource> = {}
// Convert field errors to InfoWithSource
Object.entries(fieldErrors).forEach(([key, errors]) => {
if (errors.length > 0) {
mergedErrors[key] = {
message: errors[0].message,
level: errors[0].level,
source: ErrorSources.Row,
type: errors[0].type || ErrorType.Custom
}
}
})
This only takes the first error for each field, potentially hiding important validation issues.
8. ✅ Inconsistent Error Handling for Empty Values (PARTIALLY RESOLVED)
Note: This issue has been partially resolved by standardizing the isEmpty function and error type system.
The system previously had different approaches to handling empty values:
- Some validations skipped empty values unless they're required
- Others processed empty values differently
- The
isEmptyfunction was defined multiple times with slight variations
The solution implemented:
- Standardized the
isEmptyfunction implementation - Ensured consistent error type usage for required field validations
- Made error filtering consistent across the application
Guidelines for future development:
- Always use the shared
isEmptyfunction for checking empty values - Ensure consistent handling of empty values across all validation rules
- Use the
ErrorType.Requiredtype for all required field validations
9. Tight Coupling Between Components
The validation system is tightly coupled across components:
ValidationCellneeds to understand the structure of errorsValidationTableneeds to extract and pass the right errorsValidationContainerdirectly manipulates the error structure
This makes it harder to refactor or reuse components independently.
10. Limited Error Prioritization
There's no clear prioritization of errors:
- When multiple errors exist for a field, which one should be shown first?
- Are some errors more important than others?
- The current system mostly shows the first error it finds
A more robust approach would be to have a consistent error source identification system and a clear prioritization strategy for displaying errors.
Let me explain how these hooks fit together to create the validation errors that eventually get filtered in the ValidationCell component:
The Validation Flow
-
useValidationState Hook: This is the main state management hook used by the
ValidationContainercomponent. It:- Manages the core data state (
data) - Tracks validation errors in a Map (
validationErrors) - Provides functions to update and validate rows
- Manages the core data state (
-
useValidation Hook: This is a utility hook that provides the core validation logic:
validateField: Validates a single field against its validation rulesvalidateRow: Validates an entire row, field by fieldvalidateTable: Runs table-level validationsvalidateUnique: Checks for uniqueness constraintsvalidateData: Orchestrates the complete validation process
How Errors Are Generated
Validation errors come from multiple sources:
-
Field-Level Validations: In
useValidation.tsx, thevalidateFieldfunction checks individual fields against rules like:required: Field must have a valueregex: Value must match a patternmin/max: Numeric constraints
-
Row-Level Validations: The
validateRowfunction inuseValidation.tsxruns:- Field validations for each field in the row
- Special validations for required fields like supplier and company
- Custom row hooks provided by the application
-
Table-Level Validations:
validateUniquechecks for duplicate values in fields marked as uniquevalidateTableruns custom table hooks for cross-row validations
-
API-Based Validations: In
useValidationState.tsxandValidationContainer.tsx:- UPC validation via API calls
- Item number uniqueness checks
The Error Flow
- Errors are collected in the
validationErrorsMap inuseValidationState - This Map is passed to
ValidationTableas a prop ValidationTableextracts the relevant errors for each cell and passes them toValidationCell- In
ValidationCell, the errors are filtered based on whether the cell has a value:// Updated implementation using type-based filtering const filteredErrors = React.useMemo(() => { return !isEmpty(value) ? errors.filter(error => error.type !== ErrorType.Required) : errors; }, [value, errors]);
Key Insights
-
Error Structure: Errors now have a consistent structure with type information:
type ErrorObject = { message: string; level: string; // 'error', 'warning', etc. source?: ErrorSources; // Where the error came from type: ErrorType; // The type of error (Required, Regex, Unique, etc.) } -
Error Sources: Errors can come from:
- Field validations (required, regex, etc.)
- Row validations (custom business logic)
- Table validations (uniqueness checks)
- API validations (UPC checks)
-
Error Types: Errors are now categorized by type:
ErrorType.Required: Field is required but emptyErrorType.Regex: Value doesn't match the regex patternErrorType.Unique: Value must be unique across rowsErrorType.Custom: Custom validation errorsErrorType.Api: Errors from API calls
-
Error Filtering: The filtering in
ValidationCellis now more robust:- When a field has a value, errors of type
ErrorType.Requiredare filtered out - When a field is empty, all errors are shown
- When a field has a value, errors of type
-
Performance Optimizations:
- Batch processing of validations
- Debounced updates to avoid excessive re-renders
- Memoization of computed values