Notes on Form validation in React

Last updated on 24-Nov-2024

Should one validate at field level (user moves out of field) or at form level (user submits the form)? Is there a web user interface guideline for this? If field level validations are needed then what events are used in React apps to do so?

Articles/posts below are related to above queries.

Field-level validation and error messaging, https://accessibility.perpendicularangel.com/roles/ux-designer/error-messaging-patterns/field-level-validation-and-error-messaging/ - The article advises that form's submit button should be enabled even when form has invalid data, show error message only after user leaves a field and not while he is typing in the field, continue to show the error message for a field at least until it has been corrected etc.

React onBlur event can be used for field-level validation. Example of usage of onBlur event but not for validation: https://www.geeksforgeeks.org/react-onblur-event/ 

Form Validation — Importance, Ways & Best Practices, https://clearout.io/blog/form-validation/ : Refers to field-level validation as "Before Submission or Inline Validation".


Nextjs tutorial app uses only form-level/on-submit validation for create invoice (Invoices -> Create Invoice button on top right): (my implementation of the tutorial) https://nextjs-dashboard-nine-blue-35.vercel.app/ (Dummy) Email: user@nextmail.com, Password: 123456

Banking sites like SBI and ICICI Bank Login forms also use only form-level/on-submit validation.
------------

For required to work, Submit button type has to be "submit" and/or handler may have to be specified as onSubmit in form element.
On Chrome browser, required for Select option does not show message if the select option is not visible when invoking Submit. This may be an issue for long forms which have to be scrolled to see the whole content.
required seems to run one field at a time which seems to be poorer user interface than user being shown all field errors at the same time when user submits the form.
required functionality seems to run ***before*** submit handler is invoked. So if submit handler coded validation needs to be used then, it seems that required should be commented out/not used.

Field-level validation (if that is the right term) can be done using onBlur but it makes the code more complex and also, it seems that most web app UI do not validate at field-level but validate at form submit.

Scrolling to first error when using custom validation code in submit handler seems to be an issue ... Reactjs Scroll to first error on form submission of a large form, https://stackoverflow.com/questions/61232786/reactjs-scroll-to-first-error-on-form-submission-of-a-large-form discusses problem in context of what seems to be built-in HTML validators :
element = document.getElementById("the_id") and element.scrollIntoView({behavior: 'smooth'});
----
May be a solution but that would need storing all ids.

document.querySelector("label[for=" + vHtmlInputElement.id + "]");

How to scroll to an element?, 
https://stackoverflow.com/questions/43441856/how-to-scroll-to-an-element covers using useRef to scroll to a particular element. But that may the needs for a large form with many fields.

React Hook form, https://www.react-hook-form.com/ is said to handle this issue.

--------
[****Wrong Info., it seems:] JavaScript Object properties have no inherent order. It is total luck what order a for...in loop operates.
...
I think above info. is wrong as it is contradicted by MDN below!
The traversal order, as of modern ECMAScript specification, is well-defined and consistent across implementations. Within each component of the prototype chain, all non-negative integer keys (those that can be array indices) will be traversed first in ascending order by value, then other string keys in ascending chronological order of property creation.
----

The Object.entries() static method returns an array of a given object's own enumerable string-keyed property key-value pairs.
...
Object.entries() returns an array whose elements are arrays corresponding to the enumerable string-keyed property key-value pairs found directly upon object. This is the same as iterating with a for...in loop, except that a for...in loop enumerates properties in the prototype chain as well. The order of the array returned by Object.entries() is the same as that provided by a for...in loop.

If you only need the property keys, use Object.keys() instead. If you only need the property values, use Object.values() instead.
============


If fieldErrorMessages has many string fields (one for each form field) and one boolean (errorsPresent) field, I could use the following to initialize fieldErrorMessages state variable:
tmpFEM = fieldErrorMessages
for(const key in tmpFEM)
   tmpFEM[key] = "";
tmpFEM.errorsPresent = false;
// Now tmpFEM is set to error free state
...
setFieldErrorMessages(tmpFEM);
----
In above code errorsPresent will also be made an empty string but that unwanted result is changed by tmpFEM.errorsPresent = false;
which will make errorsPresent a boolean with false value.
The code is not so clear and could probably confuse some readers. So above approach may not be appropriate for app code which has to be viewed and modified by developers who may not have much exposure to such JS code. 

--------------------------
How to use Yup validation for HTML forms in React, https://www.contentful.com/blog/yup-validate-forms-in-react/ , May 2024.
How to use Yup for Form Validation without Formik in ReactJS?, https://www.studytonight.com/post/how-to-use-yup-for-form-validation-without-formik-in-reactjs , May 2024.
===================================

Example code fragments (not tested specifically) ... Improvement in code to reduce repetitive code has to be explored when time permits:

  .error-message {
    font-size: ...;
    color: ...;
  }
...
  function handleFieldPropName1Blur(e) {
    if (e.target.value) {
      if (fieldErrorMessages.fieldPropName1) {
        const errors = {...fieldErrorMessages, fieldPropName1:""}
        setFieldErrorMessages(errors); 
      }
    } else {
      const errors = {...fieldErrorMessages, fieldPropName1:"Field Prop Name1 is required.", errorsPresent: true}
      setFieldErrorMessages(errors); 
    }
  }

  function handleFieldPropName2Blur(e) {
    if (e.target.value) {
      if (fieldErrorMessages.fieldPropName2) {
        const errors = {...fieldErrorMessages, fieldPropName2:""}
        setFieldErrorMessages(errors); 
      }
    } else {
      const errors = {...fieldErrorMessages, fieldPropName2:"Field Prop Name2 is required.", errorsPresent: true}
      setFieldErrorMessages(errors); 
    }
  }

      <div>
        <label htmlFor="fieldElmId1">Field Prop Name1</label>
        <input
          type="text"
          id="fieldElmId1"
          value={xyz.fieldPropName1}
          placeholder="..."
          onChange={(e) => ...}
          onBlur={handleFieldPropName1Blur}
        ></textarea>
        <div className="error-message">
          {fieldErrorMessages.fieldPropName1? <p>{fieldErrorMessages.fieldPropName1}</p> : null} 
        </div>
      </div>
...
      <div>
        <label htmlFor="fieldElmId2">Field Prop Name1</label>
        <input
          type="text"
          id="fieldElmId2"
          value={xyz.fieldPropName2}
          placeholder="..."
          onChange={(e) => ...}
          onBlur={handleFieldPropName2Blur}
        ></textarea>
        <div className="error-message">
          {fieldErrorMessages.fieldPropName2? <p>{fieldErrorMessages.fieldPropName2}</p> : null} 
        </div>
      </div>
...
      <div className="error-message">
        {fieldErrorMessages.errorsPresent? <p>Missing Fields. Cannot create ....</p> : null} 
      </div>

...
  class fieldErrorMessagesC {
    constructor() {
      this.fieldPropName1 = "";
      this.fieldPropName2 = "";
      this.errorsPresent = false;
    }
  }
  const [fieldErrorMessages, setFieldErrorMessages] = useState(new fieldErrorMessagesC());

...

  const handleSubmit = async (e) => {
    e.preventDefault();
    let errors = new fieldErrorMessagesC();
    ---if data field of name fieldPropName1 is empty--- {
      errors.fieldPropName1 = "fieldPropName1 is required."
      errors.errorsPresent = true;
    }
    ---if data field of name fieldPropName2 is empty--- {
      errors.fieldPropName2 = "fieldPropName2 is required."
      errors.errorsPresent = true;
    }
    setFieldErrorMessages (errors); //if no errors then initialized value is set in state variable
    if (errors.errorsPresent) {
      const fieldNameToElementIdMap = [
        ["fieldPropName1", "fieldElmId1"],
        ["fieldPropName1", "fieldElmId2"]
      ]
      const firstErrorField = fieldNameToElementIdMap.find((field) => errors[field[0]]);
      if (firstErrorField) {
        console.log("firstErrorField[1] (element id)", firstErrorField[1])  
        const firstErrorFieldElement = document.getElementById(firstErrorField[1]);
        const firstErrorLabelElement = document.querySelector("label[for=" + firstErrorFieldElement?.id + "]");
        if (firstErrorLabelElement) {
          firstErrorLabelElement.scrollIntoView({behavior: 'smooth'});
        } else if (firstErrorFieldElement) {
          firstErrorFieldElement.scrollIntoView({behavior: 'smooth'});
        }
      }
      return;
    }
...
}

============================
24 Nov. 2024:

React Formik Tutorial with Yup (React Form Validation), https://www.youtube.com/watch?v=7Ophfq0lEAY , around 35 mins, by Nikita Dev, May 2022, GH: Finished files: https://github.com/nikitapryymak/formik-tutorial/tree/finished-files, Starting files: https://github.com/nikitapryymak/formik-tutorial
[VSCode: After cloning, use 'git checkout finished-files' on local PC to switch to finished-files branch. Don't use 'git branch finished-files' followed by 'git checkout branch-name finished-files', for more details (see 24 Nov. 2024 entry).]


----------
react-select: a third-party npm component, https://www.npmjs.com/package/react-select
[react-select] React select npm onBlur function returns An error, https://stackoverflow.com/questions/69130927/react-select-npm-onblur-function-returns-an-error :
... the setValue hook is asynchronous, and the value variable isn't updated yet when the onBlur event fires, so it holds the previously set value.

So one cannot use e.target.value to check if user specified a value in the field or not. Note that for select (HTML used by React) if multiple selection option is used, it seems to return an array of options in e.target.options with each option having a value property (have not checked that out as I did not need to).[Retrieving value from <select> with multiple option in React, https://stackoverflow.com/questions/28624763/retrieving-value-from-select-with-multiple-option-in-react]
Select with S uppercase, is the name used by react-select third-party npm component.

One possible solution is to have OnChange handler save current value in state variable and then onBlur handler can pick up the value from the state variable.
====


How to Create Dynamic Values and Objects in JavaScript?, https://www.geeksforgeeks.org/how-to-create-dynamic-values-and-objects-in-javascript/ :
Approach 2: Using the bracket notation for object property assignment
...
obj[propertyName] = propertyValue;
---
Copy-paste of trial in node:
Welcome to Node.js v18.18.0.      
Type ".help" for more information.
> let o = {}
undefined
> o."prop" = 2;
o."prop" = 2;
  ^^^^^^

Uncaught SyntaxError: Unexpected string
> o["prop"] = 2;
2
> console.log(o)
{ prop: 2 }
undefined
> console.log(o.prop)
2
undefined
>
=================
The delete operator removes a property from an object. Its usage is simple and direct, making it the go-to method for many developers.
----

Trial program:
Welcome to Node.js v18.18.0.      
Type ".help" for more information.
> let o = {}
undefined
> o['dynprop']='Hi'
'Hi'
> console.log(o)
{ dynprop: 'Hi' }
undefined
> delete o['dynprop']
true
> console.log(o)
{}
undefined
>
=================

The delete operator removes a given property from an object. On successful deletion, it will return true, else false will be returned. 
...
It is important to consider the following scenarios:

If the property which you are trying to delete does not exist, delete will not have any effect and will return true.
==================
So we need not check if a property exists on an object before issuing delete. In others words, an exception is NOT thrown if delete is used with a non-existent property.

Code trial:
Welcome to Node.js v18.18.0.      
Type ".help" for more information.
> let o = {}
undefined
> if ('dynprop' in o) delete o['dynprop']
undefined
> delete o['dynprop']
true
>
========================

The Object.hasOwn() static method returns true if the specified object has the indicated property as its own property. If the property is inherited, or does not exist, the method returns false.
...
const object1 = {
  prop: 'exists',
};

console.log(Object.hasOwn(object1, 'prop'));
// Expected output: true
======================

Mandatory fields code can be minimized. I modified fieldErrorMessages state variable (mandatory fields error messages) to use dynamic properties. I need to also delete the dynamic error message property when error has been corrected and field has become valid. Related code snippets:

const mandatoryFieldNames = [
    "fieldPropName1",
    "fieldPropName2", 
];


  const [fieldErrorMessages, setFieldErrorMessages] = useState({});
...
  const handleMandatoryFieldBlur = (field, value) => {
    const errors = {...fieldErrorMessages}
    // console.log(`handleMandatoryFieldBlur: field: ${field}, value: ${value}`)
    if (value) {
      if (errors[field]) {
        delete errors[field]
        setFieldErrorMessages(errors); 
      }
    } else {
      errors[field] = "Required"
      setFieldErrorMessages(errors); 
    }
-----
The submit handler minimized code version:

  const IsFormDataValid = () => {
    let errors = {};

    // Mandatory fields check
    mandatoryFieldNames.map((fieldName)=>{
        ---if data field of name fieldName is empty--- {
          errors[fieldName] = "Required"
        }
    })

 
    setFieldErrorMessages (errors); //note that if there are no errors then initialized value is set in state variable
    ...
    // if errors are present code to scroll to first error
    ...

    return (!Object.keys(errors).length)
}

  const handleSubmit = async (e) => {
    e.preventDefault();

    if (!IsFormDataValid()) {
      return;
    }
...
====================

The Array.isArray() static method determines whether the passed value is an Array.
----



Comments