UNPKG

@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
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, }; };