Creating a Custom useForm Hook in React for Dynamic Form Validation
Published: 2024-06-22 11:14:17

Managing form state and validation in React can often become cumbersome, especially when dealing with complex forms and nested fields. To simplify this process, creating a custom useForm hook can be incredibly beneficial. In this article, we'll walk through the creation of a useForm hook that handles validation, form state management, and error handling in a reusable and dynamic manner.

**The useForm Hook**
Let's start by defining the useForm hook. This hook will manage the form's state, handle changes, reset the form, and validate fields based on the rules passed to it.

```
import { useState } from "react";
import validate from "../validate";

const useForm = (
  initialState,
  validationTypes,
  shouldValidateFieldCallback,
  getFieldDisplayName
) => {
  const [formData, setFormData] = useState(initialState);
  const [errors, setErrors] = useState({});
  const [showErrors, setShowErrors] = useState(false);

  const onHandleChange = (newFormData) => {
    setFormData(newFormData);
  };

  const onHandleReset = () => {
    setFormData(initialState);
    setErrors({});
    setShowErrors(false);
  };

  const shouldValidateField = (name) => {
    if (shouldValidateFieldCallback) {
      return shouldValidateFieldCallback(name, formData);
    }
    return true; // Default behavior: always validate if no callback provided
  };

  const validateAll = (currentFormData = formData) => {
    let allValid = true;
    const newErrors = {};

    const traverseFormData = (data) => {
      for (const key in data) {
        if (Object.prototype.hasOwnProperty.call(data, key)) {
          const value = data[key];
          const fieldName = key;

          if (typeof value === "object" && value !== null && !Array.isArray(value)) {
            traverseFormData(value);
          } else if (shouldValidateField(fieldName)) {
            const validationType = validationTypes?.[fieldName];
            if (validationType) {
              const displayName = getFieldDisplayName(fieldName);
              const errorElement = validate(value, validationType, displayName);
              if (errorElement) {
                allValid = false;
                newErrors[fieldName] = errorElement;
              }
            }
          }
        }
      }
    };

    traverseFormData(currentFormData);

    setErrors(newErrors);
    return allValid;
  };

  const onHandleSubmit = (callback) => (e) => {
    e.preventDefault();
    setShowErrors(true);
    if (validateAll()) {
      callback();
    }
  };

  return {
    formData,
    errors,
    showErrors,
    onHandleChange,
    onHandleSubmit,
    onHandleReset,
  };
};

export default useForm;
```
**Explanation**:
Initial State Management: We start by initializing the form state and errors using the useState hook.
Change Handling: onHandleChange updates the form state based on user input.
Reset Handling: onHandleReset resets the form state to its initial values and clears errors.

**Validation**: validateAll traverses the form data, checks validation rules, and sets error messages if any validation fails.
Submission Handling: onHandleSubmit triggers validation and, if successful, executes the provided callback function.
The validate Function
The validate function is responsible for performing the actual validation checks based on the rules specified.

```
import React from "react";
import { capitalize } from "lodash";
import { constant } from "../constants/constant";

const validate = (value, validationType, fieldName) => {
  if (!validationType) {
    return null; // No validation type specified
  }

  const validations = validationType.split("|");
  let errorMessage = null;

  // Patterns
  const emailPattern = constant.REGEX.BASICEMAILPATTERN;
  const alphaPattern = constant.REGEX.APLHAONLYPATTERN;

  for (const type of validations) {
    const [vType, param] = type.split(":");

    switch (vType) {
      case "required":
        if (value === "" || value === null || value === undefined) {
          errorMessage = `${capitalize(fieldName)} field is required.`;
        }
        break;
      case "email":
        if (value && !emailPattern.test(value)) {
          errorMessage = `${capitalize(fieldName)} must be a valid email address.`;
        }
        break;
      case "min":
        if (value.length < parseInt(param)) {
          errorMessage = `${capitalize(fieldName)} must be at least ${param} characters.`;
        }
        break;
      case "alphaOnly":
        if (value && !alphaPattern.test(value)) {
          errorMessage = `${capitalize(fieldName)} field must contain only alphabetic characters.`;
        }
        break;
      default:
        break;
    }
    if (errorMessage) {
      break;
    }
  }

  return errorMessage ? <div className="text-danger">{errorMessage}</div> : null;
};

export default validate;
```
Usage Example
Here's how you can use the useForm hook in a form component:

```
import React from "react";
import useForm from "./useForm"; // Adjust the import path as needed

const MyFormComponent = () => {
  const initialState = {
    UserID: 0,
    UserEmail: '',
    FirstName: '',
    LastName: '',
    LicencesData: {
      LicenseType: null,
      EnterpriseLicense: null,
      IsProTrial: null,
      CreditsBalance: null,
      EAlertCreditsAvailable: null,
      StartAt: null,
      EndAt: null,
    },
  };

  const validationTypes = {
    UserEmail: "required|email",
    FirstName: "required|alphaOnly",
    LastName: "required|alphaOnly",
    "LicencesData.LicenseType": "required",
    "LicencesData.StartAt": "required",
    "LicencesData.EndAt": "required",
  };

  const shouldValidateFieldCallback = (name, formData) => {
    if (name === "Password" && formData.IsAutogeneratePassword) {
      return false;
    }
    if (["LicencesData.StartAt", "LicencesData.EndAt"].includes(name) && formData.LicencesData.LicenseType?.value === 2) {
      return false;
    }
    return true;
  };

  const getFieldDisplayName = (fieldName) => {
    const displayNames = {
      UserEmail: "Email",
      FirstName: "First name",
      LastName: "Last name",
      "LicencesData.LicenseType": "License type",
      "LicencesData.StartAt": "Start date",
      "LicencesData.EndAt": "End date",
    };
    return displayNames[fieldName] || fieldName;
  };

  const { formData, errors, showErrors, onHandleChange, onHandleSubmit, onHandleReset } = useForm(
    initialState,
    validationTypes,
    shouldValidateFieldCallback,
    getFieldDisplayName
  );

  return (
    <form onSubmit={onHandleSubmit(() => console.log("Form submitted successfully!"))}>
      <div>
        <label>Email:</label>
        <input
          type="text"
          name="UserEmail"
          value={formData.UserEmail}
          onChange={(e) => onHandleChange({ ...formData, UserEmail: e.target.value })}
        />
        {showErrors && errors.UserEmail}
      </div>
      <div>
        <label>First Name:</label>
        <input
          type="text"
          name="FirstName"
          value={formData.FirstName}
          onChange={(e) => onHandleChange({ ...formData, FirstName: e.target.value })}
        />
        {showErrors && errors.FirstName}
      </div>
      <div>
        <label>Last Name:</label>
        <input
          type="text"
          name="LastName"
          value={formData.LastName}
          onChange={(e) => onHandleChange({ ...formData, LastName: e.target.value })}
        />
        {showErrors && errors.LastName}
      </div>
      <div>
        <label>License Type:</label>
        <input
          type="text"
          name="LicencesData.LicenseType"
          value={formData.LicencesData.LicenseType || ""}
          onChange={(e) => onHandleChange({ ...formData, LicencesData: { ...formData.LicencesData, LicenseType: e.target.value } })}
        />
        {showErrors && errors["LicencesData.LicenseType"]}
      </div>
      <div>
        <label>Start Date:</label>
        <input
          type="text"
          name="LicencesData.StartAt"
          value={formData.LicencesData.StartAt || ""}
          onChange={(e) => onHandleChange({ ...formData, LicencesData: { ...formData.LicencesData, StartAt: e.target.value } })}
        />
        {showErrors && errors["LicencesData.StartAt"]}
      </div>
      <div>
        <label>End Date:</label>
        <input
          type="text"
          name="LicencesData.EndAt"
          value={formData.LicencesData.EndAt || ""}
          onChange={(e) => onHandleChange({ ...formData, LicencesData: { ...formData.LicencesData, EndAt: e.target.value } })}
        />
        {showErrors && errors["LicencesData.EndAt"]}
      </div>
      <button type="submit">Submit</button>
      <button type="button" onClick={onHandleReset}>Reset</button>
    </form>
  );
};

export default MyFormComponent;
```
**Conclusion**
With the custom useForm hook, managing form state and validation in React becomes much more manageable. This hook allows for flexible and dynamic form handling, ensuring that your forms are easy to maintain and extend. By following the patterns outlined in this article, you can create robust form handling logic for any React application.