Search code examples
javascriptreactjsvalidation

How do I get instant validation of form fields in this React 18 app?


I am working on a chat app with React 18 and Firebase.

In the src\pages\Register.jsx component, I have a form that I validate with Simple Body Validator:

import React, { useState } from "react";
import FormCard from '../components/FormCard/FormCard';
import { make } from 'simple-body-validator';

export default function Register() {
 const initialFormData = {
    firstName: '',
    lastName: '',
    email: '',
    password: '',
    passwordConfirm: ''
 };

 const validationRules = {
    firstName: ['required', 'string', 'min:3', 'max:255'],
    lastName: ['required', 'string', 'min:3', 'max:255'],
    email: ['required', 'email'],
    password: ['required', 'confirmed'],
    passwordConfirm: ['required'],
 };

 const validator = make(initialFormData, validationRules);
 const [formData, setFormData] = useState(initialFormData);
 const [errors, setErrors] = useState(validator.errors());

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

 const handleSubmit = (event) => {
    event.preventDefault();

    if (!validator.setData(formData).validate()) {
      setErrors(validator.errors());
    }
 };
  
 return (
    <FormCard title="Register">
      <form onSubmit={handleSubmit}>
        <div className={`mb-2 form-element ${errors.has('firstName') ? 'has-error' : null}`}>
          <label for="firstName" className="form-label">First name</label>
          <input type="text" id="firstName" name="firstName" value={formData.firstName} onChange={handleChange} className="form-control form-control-sm" />
          { errors.has('firstName') ? (
          <p className="invalid-feedback">{errors.first('firstName')}</p>
        ) : null }
        </div>

        <div className={`mb-2 form-element ${errors.has('lastName') ? 'has-error' : null}`}>
          <label for="lastName" className="form-label">Last name</label>
          <input type="text" id="lastName" name="lastName" value={formData.lastName} onChange={handleChange} className="form-control form-control-sm" />
          { errors.has('lastName') ? (
          <p className="invalid-feedback">{errors.first('lastName')}</p>
        ) : null }
        </div>

        <div className={`mb-2 form-element ${errors.has('email') ? 'has-error' : null}`}>
          <label for="email" className="form-label">Email address</label>
          <input type="email" id="email" name="email" value={formData.email} onChange={handleChange} className="form-control form-control-sm" />
          { errors.has('email') ? (
          <p className="invalid-feedback">{errors.first('email')}</p>
        ) : null }
        </div>

        <div className={`mb-2 form-element ${errors.has('password') ? 'has-error' : null}`}>
          <label for="password" className="form-label">Password</label>
          <input type="password" id="password" name="password" value={formData.password} onChange={handleChange} className="form-control form-control form-control-sm" />
          { errors.has('password') ? (
          <p className="invalid-feedback">{errors.first('password')}</p>
        ) : null }
        </div>

        <div className={`mb-2 form-element ${errors.has('passwordConfirm') ? 'has-error' : null}`}>
          <label for="password_repeat" className="form-label">Confirm Password</label>
          <input type="password" id="passwordConfirm" name="passwordConfirm" value={formData.passwordConfirm} onChange={handleChange} className="form-control form-control form-control-sm" />
          { errors.has('passwordConfirm') ? (
          <p className="invalid-feedback">{errors.first('passwordConfirm')}</p>
        ) : null }
        </div>

        <div className="pt-1">
          <button type="submit" className="btn btn-sm btn-success fw-bold">Submit</button>
        </div>
      </form>
    </FormCard>
  );
 }

If the form is invalid, once a submit is adempted, the validation error messages are displayed as expected:

enter image description here

The problem

If valid data is introduced into an invalid form, the validation error messages and classes do not disappear on input/change, as I believe it should.

The valid fields pass the validation only upon a new click of the submit button while I want the feedback to be instant, similar to the AngularJS or Angular2+ forms.

Sandbox

There is a sandbox with the code here. The registration route is /auth/register.

Questions

  1. What am I doing wrong?
  2. What is the most reliable way to get the desired behavior?

Solution

  • I have noticed that the library you're using does not handle form states(e.g. dirty, pristine).

    So, here would be my approach:

    Register.tsx

    /* ... */
    const initialFormData = {
      firstName: "",
      lastName: "",
      email: "",
      password: "",
      passwordConfirm: ""
    };
    
    const [pristineFields, setPristineFields] = useState(() =>
      Object.keys(initialFormData)
    );
    
    const handleChange = (event) => {
      const { name, value } = event.target;
    
      setFormData((prevFormData) => {
        const newFormData = { ...prevFormData, [name]: value };
        const newPristineFields = pristineFields.filter((f) => f !== name);
    
        validator.setData(newFormData).validate();
        const validationErrors = validator.errors();
        // 'Forgetting' about pristine fields.
        newPristineFields.forEach((f) => validationErrors.forget(f));
        setErrors(validationErrors);
    
        setPristineFields(newPristineFields);
    
        return newFormData;
      });
    };
    
    /* ... */
    

    I have also added these lines in the submit handler:

    const handleSubmit = (event) => {
      event.preventDefault();
    
      if (!validator.setData(formData).validate()) {
        setErrors(validator.errors());
      } else {
        console.log("all good");
      }
    
      // Validations are shown either way, so we could forget
      // about any pristine fields at this point.
      setPristineFields([]);
    };
    

    Here is the link to the updated CodeSandbox application.


    Update - validating/invalidating confirm password

    const CONFIRM_PASSWORD_KEY = "passwordConfirm";
    const PASSWORD_KEY = "password";
    
    export default function Register() {
      const initialFormData = {
        firstName: "",
        lastName: "",
        email: "",
        [PASSWORD_KEY]: "",
        [CONFIRM_PASSWORD_KEY]: ""
      };
    ...
    

    Then, in the setFormData's callback, I have added the logic for password confirmation:

    ...
        setFormData((prevFormData) => {
          const newFormData = { ...prevFormData, [name]: value };
          const newPristineFields = pristineFields.filter((f) => f !== name);
    
          validator.setData(newFormData).validate();
          const validationErrors = validator.errors();
          newPristineFields.forEach((f) => validationErrors.forget(f));
    
          // Other cases can be handled here..
          if (name === CONFIRM_PASSWORD_KEY) {
            if (value !== newFormData[PASSWORD_KEY]) {
              validationErrors.add(CONFIRM_PASSWORD_KEY, {
                message: "Passwords do not match."
              });
            }
          }
    
          setErrors(validationErrors);
          setPristineFields(newPristineFields);
          return newFormData;
        });
    ...