<div id="app"></div>
.was-validated {
  .form-control {
    &:valid~.invalid-feedback {
     display: none;      
    }  
  }
}
View Compiled
/**
 * A custom Form component that handles form validation errors.
 * It executes the form's checkValidity
 **/
class Form extends React.Component {
  state = {
    isValidated: false
  };

  /**
   * Them main function that validates the form and fills in the error messages.
   * @returns bool Returns a boolean showing if the form is valid for submission or not.
   **/
  validate = () => {
    //this.formEl is a reference in the component to the form DOM element.
    const formEl = this.formEl;
    const formLength = formEl.length;

    /*
    * The checkValidity() method on a form runs the 
    * html5 form validation of its elements and returns the result as a boolean.
    * It returns 'false' if at least one of the form elements does not qualify,
    * and 'true', if all form elements are filled with valid values.
    */
    if (formEl.checkValidity() === false) {
      for (let i = 0; i < formLength; i++) {
        //the i-th child of the form corresponds to the forms i-th input element
        const elem = formEl[i];
        /*
        * errorLabel placed next to an element is the container we want to use 
        * for validation error message for that element
        */
        const errorLabel = elem.parentNode.querySelector(".invalid-feedback");

        /*
        * A form element contains also any buttuns contained in the form.
        * There is no need to validate a button, so, we'll skip that nodes.
        */
        if (errorLabel && elem.nodeName.toLowerCase() !== "button") {
          /*
          * Each note in html5 form has a validity property. 
          * It contains the validation state of that element.
          * The elem.validity.valid property indicates whether the element qualifies its validation rules or no.
          * If it does not qualify, the elem.validationMessage property will contain the localized validation error message.
          * We will show that message in our error container if the element is invalid, and clear the previous message, if it is valid.
          */
          if (!elem.validity.valid) {
            errorLabel.textContent = elem.validationMessage;
          } else {
            errorLabel.textContent = "";
          }
        }
      }

      //Return 'false', as the formEl.checkValidity() method said there are some invalid form inputs.
      return false;
    } else {
      //The form is valid, so we clear all the error messages
      for (let i = 0; i < formLength; i++) {
        const elem = formEl[i];
        const errorLabel = elem.parentNode.querySelector(".invalid-feedback");
        if (errorLabel && elem.nodeName.toLowerCase() !== "button") {
          errorLabel.textContent = "";
        }
      }

      //Return 'true', as the form is valid for submission
      return true;
    }
  };

  /**
  * This is the method that is called on form submit.
  * It stops the default form submission process and proceeds with custom validation.
  **/
  submitHandler = event => {
    event.preventDefault();

    //If the call of the validate method was successful, we can proceed with form submission. Otherwise we do nothing.
    if (this.validate()) {
      this.props.submit();
    }

    this.setState({ isValidated: true });
  };

  /**
  * Render the component as a regular form element with appended children from props.
  **/
  render() {
    const props = [...this.props];

    //Add bootstrap's 'was-validated' class to the forms classes to support its styling
    let classNames = [];
    if (props.className) {
      classNames = [...props.className];
      delete props.className;
    }

    if (this.state.isValidated) {
      classNames.push("was-validated");
    }

    //The form will have a refference in the component and a submit handler set to the component's submitHandler
    return (
      <form
        {...props}
        className={classNames}
        noValidate
        ref={form => (this.formEl = form)}
        onSubmit={this.submitHandler}
      >
        {this.props.children}
      </form>
    );
  }
}

/**
 * The Main App component class that is rendered in the main container div.
 * It renders the page layout containing a header and the form element.
 * The layout uses Bootstrap 4 classes as a visual theme.
 **/
class App extends React.Component {
  state = {
    showFormSuccess: false
  };

  /**
   * This method is the one that should handle your form submits.
   * Timpcally, it will send the form data with an ajax call to your server.
   * In react, you would usually use the axios lib for that.
   **/
  submit = () => {
    //Replace this code with a working request to your backend.
    //Now it just displays a success message.
    this.setState({showFormSuccess: true});
    setTimeout(() => {this.setState({showFormSuccess: false});}, 5000)
  }
  
  _renderSuccessMessage() {
    return (
      <div className={"alert alert-success mt-4"} role="alert">
        Form was successfully validated and is ready to be submitted!
      </div>
    );
  }

  /**
   * Renders the page UI. Uses the Form component defined above
   * instead of the usual html form.
   * Uses Bootstrap layout classes.
   **/
  render() {
    return (
      <div className={"container pt-4"}>
        <div className={"row justify-content-center"}>
          <div className={"card"}>
            <h5 className={"card-header"}>
              Validation Test Form
            </h5>
            <div className={"card-body"}>
              <Form submit={this.submit}>
                <div className={"form-group"}>
                  <label
                    htmlFor={"email"}
                    >
                    Email
                  </label>
                  <input
                    id={"email"}
                    className={"form-control"}
                    required={true}
                    name={"email"}
                    type={"email"}
                    />
                  <div className="invalid-feedback" />
                </div>
                <div className={"form-group"}>
                  <label
                    htmlFor={"password"}
                    >
                    Password
                  </label>
                  <input
                    id={"password"}
                    className={"form-control"}
                    required={true}
                    name={"password"}
                    type={"password"}
                    minLength={6}
                    pattern="(?=.*\d)(?=.*[a-z]).{6,}"
                    />
                  <small className="form-text text-muted">Must be at least 6 characters long, contain letters and numbers</small>
                  <div className="invalid-feedback" />
                </div>
                <div className={"row justify-content-md-center"}>
                  <div className={"col-sm-12"}>
                    <button
                      type={"submit"}
                      className={"btn btn-primary mb-2"}
                      >
                      Test submit!
                    </button>
                  </div>
                </div>
              </Form>
            </div>
          </div>
        </div>
        {this.state.showFormSuccess ? this._renderSuccessMessage() : null}
      </div>
    );
  }
}

//Render the App componetn inside the main div with id equal to "app"
ReactDOM.render(<App/>, document.getElementById("app"));
View Compiled

External CSS

  1. https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0/css/bootstrap.css

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/react/16.2.0/umd/react.development.js
  2. https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.2.0/umd/react-dom.development.js