skip to Main Content

I am working with a form in react, So I created custom hook for handling form and validation, which is working fine, But I recently found some issue in that, which not good for more than one form, suppose one form is login form, and then when I login I have several other forms as well which doesn’t work in this scenario.

What I did

  • My Input field component

    <form onSubmit={handleSubmit}>
    <input
        type="text"
        className={`${errors.fName && "input-error"}`}
        name="fName"
        id="fName"
        onChange={handleChange}
        value={values.fName || ""}
      />;
    </form>
    
  • I have written one useForm custom hook, to handle the changes, submit and validation as well, below is the code

    import { useState, useEffect } from "react";
    const useForm = (callback, validate) => {
    const [values, setValues] = useState({});
    const [errors, setErrors] = useState({});
    const [isSubmitting, setIsSubmitting] = useState(false);
    
    useEffect(() => {
      if (Object.keys(errors).length === 0 && isSubmitting) {
        callback();
      }
    }, [errors]);
    
    const handleSubmit = (event) => {
      if (event) event.preventDefault();
      setErrors(validate(values));
      setIsSubmitting(true);
    };
    
    const handleChange = (event) => {
      event.persist();
      setValues((values) => ({
        ...values,
        [event.target.name]: event.target.value,
      }));
    };
    
    return {
      handleChange,
      handleSubmit,
      values,
      errors,
    };
    };
    export default useForm;
    
  • Now below is my validation.js file

    export default function validate(values) {
    let errors = {};
    if (!values.emailAddress) {
      errors.emailAddress = "Email address is required";
    }
    
    if (!values.password) {
      errors.password = "Password is required";
    }
    if (!values.fName) {
      errors.fName = "first name is required";
    }
    if (!values.lName) {
      errors.lName = "last name is required";
    }
    return errors;
    

    }

  • Now In my main component where Input fields are declarer I am calling the useform hook and passing the click and validate function, and click function will only work after validate function. Below is how I am doing

      const { values, errors, handleChange, handleSubmit } = useForm(
      btnClick,
      Validate
      );
    

Steps to recreate the issue

  • In validation file I have four validate rules which are for email address, password, firstname, lastname
  • Now email address and password are in one form and firstname and lastname are in other form
  • So when I click submit in form one it doesn’t give me the values because for this form two errors still exist
  • My thinkin is whichever form I am click submit it should validate those rules only not other form rules

What I am looking for

  • If I can do it with useRef/ref like how https://react-hook-form.com/ is doing where there is no onchange, and if want not to give validation to any of input I will just not pass ref

  • I like @TusharShahi answer and will follow that, But I am looking for more options which is more of generic code

2

Answers


  1. So this can definitely be solved using refs and modifying the validate function a bit.

    While this can be done by using a single validate function, do remember it is necessary to pass to the function what field values it should be validating. Without that it would not be aware of which values to check and it would be checking for all the values.

    This is how I would modify the validate function:

    const errMap = {
      emailAddress: "Email address is required",
      password: "Password is required",
    
      fName: "first name is required",
      lName: "last name is required"
    };
    
    function validate(values, namesToCheck) {
      let errors = {};
    
      for (let i = 0; i < namesToCheck.length; i++) {
        if (!values[namesToCheck[i]]) {
          errors[namesToCheck[i]] = errMap[namesToCheck[i]];
        }
      }
      return errors;
    }
    

    errMap is just an easy config object to tell what each field value’s error would look like. The key is the name of the field and the value is the error message.

    Since our solution need to be extensible enough to the situation when not all fields in a single form need to be validated, the hook should also change. It needs to allow passing only the fields which need to be checked.

    const useForm = (callback, validate, inputsToCheck) => {
      const [values, setValues] = useState({});
      const [errors, setErrors] = useState({});
      const [isSubmitting, setIsSubmitting] = useState(false);
    
      useEffect(() => {
        if (Object.keys(errors).length === 0 && isSubmitting) {
          callback();
        }
      }, [errors]);
    
      const handleSubmit = (event) => {
        if (event) event.preventDefault();
    
        let namesToCheck = inputsToCheck.map(({ current: { name } = {} }) => name);
    
        setErrors(validate(values, namesToCheck));
        setIsSubmitting(true);
      };
    
      const handleChange = (event) => {
        event.persist();
        setValues((values) => ({
          ...values,
          [event.target.name]: event.target.value
        }));
      };
    
      return {
        handleChange,
        handleSubmit,
        values,
        errors
      };
    };
    

    There are two changes:

    1. A new param has been added which contains all the refs of the inputs to check.
    2. handleSubmit is modified to extract names that need to pass to validate.

    Finally, passing the correct refs to the hook will complete the solution.

    const Form2 = () => {
      const btn2Click = () => {
        console.log("form2", values);
      };
      const fnameRef = useRef();
      const lnameRef = useRef();
    
      const { values, errors, handleChange, handleSubmit } = useForm(
        btn2Click,
        validate,
        [fnameRef, lnameRef]
      );
      return (
        <div>
          <form onSubmit={handleSubmit}>
            <div className="row">
              <div className="input-group">
                <label>First name</label>
                <input
                  ref={fnameRef}
                  type="text"
                  className={`${errors.fName && "input-error"}`}
                  name="fName"
                  id="fName"
                  onChange={handleChange}
                  value={values.fName || ""}
                />
                <p className="error-text">{errors.fName}</p>
              </div>
    
              <div className="input-group">
                <label>last name</label>
                <input
                  ref={lnameRef}
                  type="text"
                  className={`${errors.lName && "input-error"}`}
                  name="lName"
                  id="lName"
                  onChange={handleChange}
                  value={values.lName || ""}
                />
                <p className="error-text">{errors.lName}</p>
              </div>
            </div>
            <button type="submit" className="btn">
              Click 2
            </button>
          </form>
        </div>
      );
    };
    

    codesandbox link

    PS:
    I do not see any problem in creating two validate functions. Both can reside in the same file too. And then you can pass the correct one when invoking your hook. With the individual validate functions you need not pass the list of inputs which need to validated as your individual validate function would be catered to your form fields. I would also like to suggest that your individual validate methods should reside close to your forms. Form1 with Validate1 (in a separate file if needed).

    Login or Signup to reply.
  2. You haven’t shown the full example, but it sounds like you’re sharing the same state between two forms.

    Hooks create new state variables each time they are called. So if you want two forms with separate state, you need to call the hook twice:

      const {
        values: values1,
        errors: errors1,
        handleChange: handleChange1,
        handleSubmit: handleSubmit1,
      } = useForm(btnClick, validate);
    
      const {
        values: values2,
        errors: errors2,
        handleChange: handleChange2,
        handleSubmit: handleSubmit2,
      } = useForm(btnClick, validate);
    

    Then you can give each form it’s respective properties.

    Note: I don’t know what btnClick is, so no idea if you need two copies of that.

    If something doesn’t make sense, then please show the full example with both forms.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search