@matthew.ngo/reform
Version:
A flexible and powerful React form management library with advanced validation, state observation, and multi-group support
188 lines (161 loc) • 5.35 kB
text/typescript
import { useEffect, useRef } from 'react';
import { UseFormStateReturn } from 'react-hook-form';
import {
FormStateChangeHandler,
FormStateObserverOptions,
FormStateObserverReturn,
FormStateSubscription,
} from './types';
import { useMemoizedCallback } from '../../common/useMemoizedCallback';
import { FormGroup } from '../../core/form/form-groups';
/**
* Hook for observing form state changes without causing unnecessary re-renders
*
* @template T - Form data type
* @param formState - React Hook Form formState object
* @param options - Configuration options
* @returns Object with form state observer utilities
*
* @example
* const form = useReform(config);
* const { formState } = form.formMethods;
* const stateObserver = useFormStateObserver(formState, {
* onDirtyChange: (isDirty) => {
* if (isDirty) saveFormDraft(form.getValues());
* },
* onErrorsChange: (hasErrors, errors) => {
* if (hasErrors) console.log('Form has errors:', errors);
* }
* });
*/
export const useFormStateObserver = <T extends Record<string, any>>(
formState: UseFormStateReturn<{ groups: FormGroup<T>[] }>,
options: FormStateObserverOptions<T> = {}
): FormStateObserverReturn<T> => {
// Store previous state for comparison
const prevStateRef = useRef<Partial<
UseFormStateReturn<{ groups: FormGroup<T>[] }>
> | null>(null);
// Store current state
const currentStateRef = useRef<
UseFormStateReturn<{ groups: FormGroup<T>[] }>
>(formState);
// Store active subscriptions
const subscriptionsRef = useRef<Set<FormStateChangeHandler<T>>>(new Set());
// Helper to check if objects are different
const isDifferent = (a: any, b: any): boolean => {
if (a === b) return false;
if (typeof a !== typeof b) return true;
if (typeof a !== 'object' || a === null || b === null) return true;
// For simple objects, use JSON comparison
// This is a simplification - for complex objects you might need a deeper comparison
return JSON.stringify(a) !== JSON.stringify(b);
};
// Add options callbacks to subscriptions
useEffect(() => {
if (options.onChange) {
subscriptionsRef.current.add(options.onChange);
}
return () => {
if (options.onChange) {
subscriptionsRef.current.delete(options.onChange);
}
};
}, [options.onChange]);
// Watch for form state changes
useEffect(() => {
const currentState = { ...formState };
const prevState = prevStateRef.current;
// Update current state reference
currentStateRef.current = currentState;
// Notify all subscribers
subscriptionsRef.current.forEach(handler => {
handler(currentState, prevState);
});
// Handle specific state changes
if (options.onDirtyChange && prevState?.isDirty !== currentState.isDirty) {
options.onDirtyChange(currentState.isDirty);
}
if (
options.onErrorsChange &&
isDifferent(prevState?.errors, currentState.errors)
) {
options.onErrorsChange(
Object.keys(currentState.errors).length > 0,
currentState.errors
);
}
if (
options.onSubmitCountChange &&
prevState?.submitCount !== currentState.submitCount
) {
options.onSubmitCountChange(currentState.submitCount);
}
if (
options.onTouchedChange &&
isDifferent(prevState?.touchedFields, currentState.touchedFields)
) {
options.onTouchedChange(
Object.keys(currentState.touchedFields || {}).length > 0,
currentState.touchedFields as any
);
}
if (options.onValidChange && prevState?.isValid !== currentState.isValid) {
options.onValidChange(currentState.isValid);
}
if (
options.onSubmittingChange &&
prevState?.isSubmitting !== currentState.isSubmitting
) {
options.onSubmittingChange(currentState.isSubmitting);
}
// Update previous state reference
prevStateRef.current = currentState;
}, [formState, options]);
// Subscribe function for external components
const subscribe = useMemoizedCallback(
(handler: FormStateChangeHandler<T>): FormStateSubscription => {
subscriptionsRef.current.add(handler);
return {
unsubscribe: () => {
subscriptionsRef.current.delete(handler);
},
};
},
[]
);
// Get current form state
const getState = useMemoizedCallback(() => {
return { ...currentStateRef.current };
}, []);
// Get previous form state
const getPreviousState = useMemoizedCallback(() => {
return prevStateRef.current ? { ...prevStateRef.current } : null;
}, []);
// Check if form is dirty
const isDirty = useMemoizedCallback(() => {
return currentStateRef.current.isDirty;
}, []);
// Check if form has errors
const hasErrors = useMemoizedCallback(() => {
return Object.keys(currentStateRef.current.errors).length > 0;
}, []);
// Check if form is submitting
const isSubmitting = useMemoizedCallback(() => {
return currentStateRef.current.isSubmitting;
}, []);
// Check if form is valid
const isValid = useMemoizedCallback(() => {
return currentStateRef.current.isValid;
}, []);
// Return the observer interface
return {
subscribe,
getState,
getPreviousState,
isDirty,
hasErrors,
isSubmitting,
isValid,
};
};