Source

hooks/useForm.jsx

import { useState } from 'react';

/**
 * Custom React hook for managing form state, including values, errors, and touched status.
 * It supports basic validation based on a provided schema.
 * @param {object} initialValues - The initial values for the form fields.
 * @param {object} validationSchema - An optional schema for validating form fields. Each key is a field name, and the value is an object of validation rules (e.g., required, minLength, email, phone).
 **/
export const useForm = (initialValues, validationSchema) => {
  const [values, setValues] = useState(initialValues);
  const [errors, setErrors] = useState({});
  const [touched, setTouched] = useState({});

  const handleChange = (e) => {
    const { name, value, type, checked } = e.target;
    let newValue = type === 'checkbox' ? checked : value;

    // Cast select value for role to integer
    if (name === 'role' && type === 'select-one') {
      newValue = value === '' ? '' : parseInt(value, 10);
    }

    setValues(prev => ({
      ...prev,
      [name]: newValue
    }));

    // Clear error when user starts typing
    if (errors[name]) {
      setErrors(prev => ({
        ...prev,
        [name]: ''
      }));
    }
  };

  const handleBlur = (e) => {
    const { name } = e.target;
    setTouched(prev => ({
      ...prev,
      [name]: true
    }));
  };

  /**
   * Validates the current form values against the provided validation schema.
   * Updates the errors state and returns true if the form is valid, false otherwise.
   */
  const validate = () => {
    if (!validationSchema) return true;
    
    const newErrors = {};
    
    Object.keys(validationSchema).forEach(field => {
      const rules = validationSchema[field];
      const value = values[field];
      
      if (rules.required && (!value || value.toString().trim() === '')) {
        newErrors[field] = `${field} is required`;
        return;
      }
      
      if (rules.minLength && value && value.length < rules.minLength) {
        newErrors[field] = `${field} must be at least ${rules.minLength} characters`;
        return;
      }
      
      if (rules.email && value && !/\S+@\S+\.\S+/.test(value)) {
        newErrors[field] = 'Please enter a valid email address';
        return;
      }
      
      if (rules.phone && value && !/^\+?[\d\s-()]+$/.test(value)) {
        newErrors[field] = 'Please enter a valid phone number';
        return;
      }
    });
    
    setErrors(newErrors);
    return Object.keys(newErrors).length === 0;
  };

  /**
   * Resets the form state back to the initial values, clearing errors and touched status.
   */
  const reset = () => {
    setValues(initialValues);
    setErrors({});
    setTouched({});
  };

  return {
    values,
    /** The current values of the form fields. */
    errors,
    /** An object containing validation errors for each field. */
    touched,
    /** An object indicating whether each field has been touched (blurred). */
    handleChange,
    /**
 * Handler function for input changes. Updates the values state and clears related errors.
 * @param {object} e - The change event object.
     */
    handleBlur,
    /** Handler function for input blur events. Updates the touched state. */
    validate,
    /** Function to trigger form validation. */
    reset,
    setValues
  };
};