Stephen Phillips
Stephen Phillips

Reputation: 681

React Hooks Form Validation OnBlur triggers all fields instead of just specific field

I am currently using this amazing hook I found a couple months ago on the internet somewhere and can't find the article I found it anymore, but basically the issue I have is that when I have a large form and I tab to the next field if I use the onBlur it runs the validation for every field so all fields that are required automatically become red and show an error (because they have nothing in them and my error css).

I want to wait until the field has at least been entered into and then left before the validation related to that specific field.

Basically I want to know if there is a way I can somehow wait until the input has been selected at least once. The only thing I can think of would be having a separate hook for each input in the form or somehow attaching an event to each field- true/false for if it has been selected at least once, but don't know how that would get implemented.

import { useState, useEffect } from "react";

const useFormValidation = (initialState, validate, authenticate) => {
    const [values, setValues] = useState(initialState);
    const [errors, setErrors] = useState({});
    const [isSubmitting, setSubmitting] = useState(false);

    useEffect(() => {
        if (isSubmitting) {
            const noErrors = Object.keys(errors).length === 0;
            if (noErrors) {
                authenticate();
                setSubmitting(false);
            } else {
                setSubmitting(false);
            }
        }
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [errors]);

    const handleChange = (event) => {
        setValues({
            ...values,
            [event.target.name]: event.target.value
        });
    }



    const handleChangeChecked = (event) => {
        setValues({...values, [event.target.name] : event.target.checked });
    }

    //THIS IS THE FUNCTION I AM TALKING ABOUT
    const handleBlur = () => {
        const validationErrors = validate(values);
        setErrors(validationErrors);
    }

    const handleSubmit = (event) => {
        event.preventDefault();
        const validationErrors = validate(values);
        setErrors(validationErrors);
        setSubmitting(true);
    }

    return {
        handleSubmit,
        handleChange,
        handleChangeChecked,
        handleBlur,
        values,
        errors,
        isSubmitting
    };
}

export default useFormValidation;

Here is a sample validation also which is the second field passed into useFormValidation function.

const validateCareTeam = (values) => {
  let errors = {};

  // First Name Errors
  if (!values.firstName) {
    errors.firstName = "First name is required";
  }

  // Last Name Errors
  if (!values.lastName) {
    errors.lastName = "Last name is required";
  }

// Email Error
if (values.email && !/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(values.email)) {
  errors.email = "Please enter a valid email address";
}

const phoneno = /^\(?([0-9]{3})\)?[-. ]?([0-9]{3})[-. ]?([0-9]{4})$/
// Phone Number Errors
if (!values.phone) {
  errors.phoneNumber = "Phone number is required";
} else if (!values.phone.match(phoneno)) {
  errors.phoneNumber = "Please enter a phone number with 10 digits.  1 not necessary"
}

  return errors;
}

export default validateCareTeam;

So basically if the use tabs after first name then all other required fields- last name and phone number turn red.

I would prefer to not event run the validation until the submit button is hit, but I have been asked for validation immediately.

Upvotes: 5

Views: 9258

Answers (2)

Stephen Phillips
Stephen Phillips

Reputation: 681

Medet Tleukabiluly got me going in the right direction with the brilliant spread operator and was close and I couldn't have done it without the code above, but since there is no change if the user tabs to the next section without putting in any inputs the if statement actually needs to be in the handleBlur function above touchedErrors. Other changes include not needing touch to be in the return block and submit function. Additionally, it doesn't update touch right away which is a function of this post - basically it doesn't run the error until it is 2 inputs down. The fix for this is to add a new useEffectHook that runs each time touched is changed.

Here is the working code, which ultimately should be using useCallbacks to get rid of the eslint disabling, but works for me for now.

import { useState, useEffect } from "react";

const useFormValidation = (initialState, validate, authenticate) => {
    const [values, setValues] = useState(initialState);
    const [errors, setErrors] = useState({});
    const [touched, setTouched] = useState([]);
    const [isSubmitting, setSubmitting] = useState(false);

    useEffect(() => {
        if (isSubmitting) {
            const noErrors = Object.keys(errors).length === 0;
            if (noErrors) {
                setTouched([]);
                authenticate();
                setSubmitting(false);
            } else {
                setSubmitting(false);
            }
        }
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [errors]);
   
    // need to rerun after there is a changed to touched
    // this checks to see if there are any errors that should be highlighted
    useEffect(() => {
            const validationErrors = validate(values);
            const touchedErrors = Object.keys(validationErrors)
            .filter(key => touched.includes(key)) // get all touched keys
            .reduce((acc, key) => {
             if (!acc[key]) {
                acc[key] = validationErrors[key]
            }
            return acc
             }, {})
            setErrors(touchedErrors);
        // eslint-disable-next-line react-hooks/exhaustive-deps

    }, [touched, values]);

    const handleChange = (event) => {
        console.log("event changed")
        setValues({
            ...values,
            [event.target.name]: event.target.value
        });
    }

    const handleChangeChecked = (event) => {
        setValues({...values, [event.target.name] : event.target.checked });
        if (!touched.includes(event.target.name)) {
            setTouched([
              ...touched,
              event.target.name
            ])
          }
    }
  
    const handleBlur = (event) => {
        if (!touched.includes(event.target.name)) {
            setTouched([
              ...touched,
              event.target.name
            ])
          }
    }

    const handleSubmit = (event) => {
        event.preventDefault();
        const validationErrors = validate(values);
        setErrors(validationErrors);
        setSubmitting(true);
    }

    return {
        handleSubmit,
        handleChange,
        handleChangeChecked,
        handleBlur,
        values,
        errors,
        isSubmitting
    };
}

export default useFormValidation;

Upvotes: 1

Medet Tleukabiluly
Medet Tleukabiluly

Reputation: 11930

Below implementation is not tested, but should give idea how it should work, read comments in code

import {
  useState,
  useEffect
} from "react";

const useFormValidation = (initialState, validate, authenticate) => {
  const [values, setValues] = useState(initialState);
  const [errors, setErrors] = useState({});
  const [touched, setTouched] = useState([]); // NEW: stores all touched field names
  const [isSubmitting, setSubmitting] = useState(false);

  useEffect(() => {
    if (isSubmitting) {
      const noErrors = Object.keys(errors).length === 0;
      if (noErrors) {
        // NEW: probably need to flush touched
        // setTouched([])
        authenticate();
        setSubmitting(false);
      } else {
        setSubmitting(false);
      }
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [errors]);

  const handleChange = (event) => {
    setValues({
      ...values,
      [event.target.name]: event.target.value
    });
    if (!touched.includes(event.target.name)) { // NEW
      setTouched([
        ...touched,
        event.target.name
      ])
    }
  }



  const handleChangeChecked = (event) => {
    setValues({ ...values,
      [event.target.name]: event.target.checked
    });
    if (!touched.includes(event.target.name)) { // NEW: check if we touched this field, then add to touched array
      setTouched([
        ...touched,
        event.target.name
      ])
    }
  }

  //THIS IS THE FUNCTION I AM TALKING ABOUT
  const handleBlur = () => {
    const validationErrors = validate(values);

    const touchedErrors = Object.keys(validationErrors) // NEW
      .filter(key => touched.includes(key)) // get all touched keys
      .reduce((acc, key) => {
        if (!acc[key]) {
          acc[key] = validationErrors[key]
        }
        return acc
      }, {})
    setErrors(touchedErrors); // NEW: touchedErrors has errors that are touched
  }

  const handleSubmit = (event) => {
    event.preventDefault();
    const validationErrors = validate(values);
    // NEW do the same
    const touchedErrors = Object.keys(validationErrors) // NEW
      .filter(key => touched.includes(key)) // get all touched keys
      .reduce((acc, key) => {
        if (!acc[key]) {
          acc[key] = validationErrors[key]
        }
        return acc
      }, {})
    setErrors(touchedErrors); // NEW: touchedErrors has errors that are touched
    setSubmitting(true);
  }

  return {
    handleSubmit,
    handleChange,
    handleChangeChecked,
    handleBlur,
    values,
    touched, // NEW: just extend API
    errors,
    isSubmitting
  };
}

export default useFormValidation;

Upvotes: 2

Related Questions