arbret/frontend/app/hooks/useForm.ts

97 lines
2.6 KiB
TypeScript
Raw Permalink Normal View History

import { useState, useCallback, useMemo } from "react";
/**
* Hook for managing form state with validation and dirty checking.
* Handles common form patterns like tracking changes, validation, and submission.
*
* @param initialData - Initial form data
* @param validator - Validation function that returns field errors
* @param onSubmit - Submit handler function
* @returns Form state and handlers
*/
export function useForm<
TData extends Record<string, unknown>,
TErrors extends Record<string, string | undefined>,
>(
initialData: TData,
validator: (data: TData) => TErrors,
onSubmit: (data: TData) => Promise<void>
): {
formData: TData;
setFormData: React.Dispatch<React.SetStateAction<TData>>;
errors: TErrors;
setErrors: React.Dispatch<React.SetStateAction<TErrors>>;
isDirty: boolean;
isValid: boolean;
isSubmitting: boolean;
handleChange: (field: keyof TData) => (e: React.ChangeEvent<HTMLInputElement>) => void;
handleSubmit: (e: React.FormEvent) => Promise<void>;
reset: () => void;
} {
const [formData, setFormData] = useState<TData>(initialData);
const [originalData] = useState<TData>(initialData);
const [errors, setErrors] = useState<TErrors>({} as TErrors);
const [isSubmitting, setIsSubmitting] = useState(false);
const isDirty = useMemo(() => {
return JSON.stringify(formData) !== JSON.stringify(originalData);
}, [formData, originalData]);
const isValid = useMemo(() => {
return Object.keys(errors).length === 0;
}, [errors]);
const handleChange = useCallback(
(field: keyof TData) => (e: React.ChangeEvent<HTMLInputElement>) => {
setFormData((prev) => ({ ...prev, [field]: e.target.value }));
// Clear error for this field when user starts typing
setErrors((prev) => {
const newErrors = { ...prev };
delete newErrors[field as keyof TErrors];
return newErrors;
});
},
[]
);
const handleSubmit = useCallback(
async (e: React.FormEvent) => {
e.preventDefault();
// Validate all fields
const validationErrors = validator(formData);
setErrors(validationErrors);
if (Object.keys(validationErrors).length > 0) {
return;
}
setIsSubmitting(true);
try {
await onSubmit(formData);
} finally {
setIsSubmitting(false);
}
},
[formData, validator, onSubmit]
);
const reset = useCallback(() => {
setFormData(originalData);
setErrors({} as TErrors);
}, [originalData]);
return {
formData,
setFormData,
errors,
setErrors,
isDirty,
isValid,
isSubmitting,
handleChange,
handleSubmit,
reset,
};
}