starkon
Version:
Complete Next.js boilerplate with authentication, i18n & CLI - Create production-ready apps instantly
167 lines (141 loc) • 4.51 kB
text/typescript
import { useEffect, useState } from 'react'
import { FieldValues, UseFormWatch } from 'react-hook-form'
/**
* Hook to detect if form values have changed from initial values
* @param watch - React Hook Form watch function
* @param initialValues - Initial form values to compare against
* @param options - Optional configuration
* @returns Object containing isChanged state and helper functions
*
* @example
* const { isChanged, resetChangeState } = useFormChanged(watch, initialValues);
* <Button disabled={!isChanged}>Save</Button>
*/
export function useFormChanged<T extends FieldValues>(
watch: UseFormWatch<T>,
initialValues: T | null | undefined,
options?: {
/**
* Fields to exclude from comparison
* Useful for fields that should not trigger the changed state
*/
excludeFields?: (keyof T)[]
/**
* Enable deep comparison for nested objects and arrays
* Default: true
*/
deepCompare?: boolean
/**
* Custom comparison function
* Return true if values are equal, false if different
*/
customCompare?: (current: T, initial: T) => boolean
},
) {
const [isChanged, setIsChanged] = useState(false)
const currentValues = watch()
useEffect(() => {
// If no initial values, consider as unchanged
if (!initialValues) {
setIsChanged(false)
return
}
// Use custom compare function if provided
if (options?.customCompare) {
const areEqual = options.customCompare(currentValues as T, initialValues)
setIsChanged(!areEqual)
return
}
// Compare current values with initial values
const hasChanges = compareValues(
currentValues as T,
initialValues,
options?.excludeFields,
options?.deepCompare ?? true,
)
setIsChanged(hasChanges)
}, [currentValues, initialValues, options?.excludeFields, options?.deepCompare, options?.customCompare])
/**
* Manually reset the changed state
* Useful when you want to mark form as unchanged after save
*/
const resetChangeState = () => {
setIsChanged(false)
}
/**
* Manually set the changed state
*/
const setChangeState = (changed: boolean) => {
setIsChanged(changed)
}
return {
isChanged,
resetChangeState,
setChangeState,
}
}
/**
* Deep comparison function for form values
*/
function compareValues<T extends FieldValues>(
current: T,
initial: T,
excludeFields?: (keyof T)[],
deepCompare = true,
): boolean {
const currentKeys = Object.keys(current) as (keyof T)[]
const initialKeys = Object.keys(initial) as (keyof T)[]
// Get all unique keys
const allKeys = new Set([...currentKeys, ...initialKeys])
for (const key of allKeys) {
// Skip excluded fields
if (excludeFields?.includes(key)) {
continue
}
const currentValue = current[key]
const initialValue = initial[key]
// Check if values are different
if (!areValuesEqual(currentValue, initialValue, deepCompare)) {
return true // Changed
}
}
return false // No changes
}
/**
* Check if two values are equal
* Handles primitives, objects, arrays, dates, etc.
*/
function areValuesEqual(value1: unknown, value2: unknown, deepCompare: boolean): boolean {
// Same reference or both null/undefined
if (value1 === value2) return true
// One is null/undefined and other is not
if (value1 == null || value2 == null) return value1 === value2
// Handle Date objects
if (value1 instanceof Date && value2 instanceof Date) {
return value1.getTime() === value2.getTime()
}
// Handle arrays
if (Array.isArray(value1) && Array.isArray(value2)) {
if (value1.length !== value2.length) return false
if (!deepCompare) {
return JSON.stringify(value1) === JSON.stringify(value2)
}
return value1.every((item, index) => areValuesEqual(item, value2[index], deepCompare))
}
// Handle objects
if (typeof value1 === 'object' && typeof value2 === 'object') {
if (!deepCompare) {
return JSON.stringify(value1) === JSON.stringify(value2)
}
const keys1 = Object.keys(value1 as object)
const keys2 = Object.keys(value2 as object)
if (keys1.length !== keys2.length) return false
return keys1.every((key) => {
const obj1 = value1 as Record<string, unknown>
const obj2 = value2 as Record<string, unknown>
return areValuesEqual(obj1[key], obj2[key], deepCompare)
})
}
// For primitives and other types
return value1 === value2
}