1

I created the following form with validation using React:

  • the data shall be validated at the time of input
  • the data shall be validated again before submission
  • all data all fields are mandatory and the data is valid

This program works, but I have the following problem:
I monitor the data check with onBlur, but when the user enters invalid data in the first field along with the error message for the first field ("Only letters"), an error message is displayed for the second field ("This field is a required").

How can I improve this my example to:

  1. at the time of input - an error message ('This field is required' or a particular message for an invalid data) was displayed only if the user touched a particular field
  2. if a button "Submit" was pressed - then error messages should be displayed near all fields with invalid data

My code:

const ErrorOutput = ({ error }) => <span>{error}</span>

class FormItem extends React.Component {
  render() {
    return (
      <div>
        <label>
          {this.props.label}
        </label>
        <input
          {...this.props.input}
        />
        {this.props.error && <ErrorOutput error={this.props.error} />}
      </div>
    );
  }
}

class App extends React.Component {
  constructor(props){
    super(props)

    this.state = {
      firstName: '',
      telNo: '',
      submit: false,
      errors: {
        firstName: '',
        telNo: '',
      },
      invalid: false,
    }
  }

  handleSubmit(e){
    e.preventDefault()
    if (this.validate()) {
      console.log('Error!')
    } else {
      console.log('Success!')
    }
  }

  validate = () => {
    const { firstName, telNo } = this.state
    const errors = {}
    let invalid = false;
    if (firstName === '') {
      errors.firstName = 'first name is required'
      invalid = true;
    } else if (!firstName.match(/^[a-zA-Z]+$/)) {
      errors.firstName = 'Letters only'
      invalid = true;
    }  
    if (telNo === '') {
      errors.telNo = 'Phone is required'
      invalid = true;
    } else if (!telNo.match(/^[0-9]+$/)) {
      errors.telNo = 'Numbers only'
      invalid = true;
    }
    this.setState({
      invalid,
      errors,
    })
    
    return invalid;
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit.bind(this)}>
        <FormItem label='First name:' input={{
            type: 'text',
            name: 'firstName',
            value: this.state.firstName,
            onChange: e => this.setState({ firstName: e.target.value }),
            onBlur: () => this.validate(),
          }} error = {this.state.errors.firstName}
        />
        <FormItem label='Phone number:' input={{
            type: 'tel',
            name: 'telNo',
            value: this.state.telNo,
            onChange: e => this.setState({ telNo: e.target.value }),
            onBlur: () => this.validate(),
          }} error = {this.state.errors.telNo}
        />
        <button>
          Submit
        </button> 
      </form>
    )
  }
}

ReactDOM.render(
  <App />,
  document.getElementById('root')
)
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<body>
<div id="root"></div>
</body>

1
  • 1
    if you create jsfiddle for this, it will be easy to check Commented May 25, 2018 at 10:42

3 Answers 3

2

Please check my example from your modified code. I've tried to simplify your logic a bit, and make it more readable and generic.

const ErrorOutput = ({ error }) => <span> {error} </span>;

const FormItem = ({ label, input, error }) => (
  <div>
    <label> {label} </label> <input {...input} />
    {error && <ErrorOutput error={error} />}
  </div>
);

class App extends React.Component {
  state = {
    firstName: "",
    telNo: "",
    submit: false,
    errors: {}
  };

  handleSubmit = () => {
    const { firstName, telNo, errors } = this.state;
    const { firstNameError, telNoError } = errors;

    const firstNameIsValid = firstName && !firstNameError;
    const telNoIsValid = telNo && !telNoError;

    firstNameIsValid && telNoIsValid
      ? console.log("Success!")
      : console.log("Error!");
  };

  handleInput = event => {
    this.setState({
      [event.target.name]: event.target.value
    });
  };

  validate = () => {
    const { firstName, telNo } = this.state;

    let errors = {};

    // Name error checking
    switch (true) {
      case !firstName:
        errors.firstNameError = "First name is required";
        break;
      case !firstName.match(/^[a-zA-Z]+$/):
        errors.firstNameError = "First name can have only letters";
        break;
      case firstName.length <= 2:
        errors.firstNameError =
          "First name needs to be at least 2 characters long";
        break;
      default:
        errors.firstNameError = "";
        break;
    }

    switch (true) {
      case !telNo:
        errors.telNoError = "Phone is required";
        break;
      case !telNo.match(/^[0-9]+$/):
        errors.telNoError = "Phone number can have only numbers";
        break;
      case telNo.length <= 8:
        errors.telNoError =
          "Telephone number needs to be at least 8 characters long";
        break;
      default:
        errors.telNoError = "";
        break;
    }

    this.setState({
      errors
    });
  };

  render() {
    const { firstName, telNo, errors } = this.state;
    return (
      <form>
        <FormItem
          label="First name:"
          input={{
            type: "text",
            name: "firstName",
            value: firstName,
            onChange: this.handleInput,
            onBlur: this.validate
          }}
          error={errors.firstNameError}
        />
        <FormItem
          label="Phone number:"
          input={{
            type: "tel",
            name: "telNo",
            value: telNo,
            onChange: this.handleInput,
            onBlur: this.validate
          }}
          error={errors.telNoError}
        />
        <button type="button" onClick={this.handleSubmit}>
          Submit
        </button>
      </form>
    );
  }
}

ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>

Also, avoid using lambda functions in the render method to avoid performance bottlenecks, as lambdas are regenerated on each render and can cause additional unintended re-renders.

Sign up to request clarification or add additional context in comments.

4 Comments

Thanks, now the code is actually more readable. But I can't really work it out still, how should I solve the problem from my question. How can I switch the part of the error checking logic to the formItem component and prevent the repeating of the same data validation before submission?
@Herasimenak I'll edit the example during the day to include that.
You still have to do some form of data validation before you submit, even if you move the logic for validating each field to the field component.
Ok, I've updated the code. There is no sense switching the validation to FormItem then, as you still need to validate the fields somehow on the parent for submission reasons. I've left the validation logic inside the App component, however the validation for submission is different. Check the handleSubmit() method that is updated for an example.
1

The problem is that when ever an onBlur event occurs your valitade method tries to validate all the fields. To solve this issue you can you can pass the event to the validate method and then only validate the field that raised the event using

event.target.name

so your validate method will look something like this

validate = e => {
  const { firstName, telNo } = this.state
  const errors = {}
  let invalid = false
  if (e && e.target.name == "firstName") {
    if (firstName === "") {
      errors.firstName = "first name is required"
      invalid = true
    } else if (!firstName.match(/^[a-zA-Z]+$/)) {
      errors.firstName = "Letters only"
      invalid = true
    }
  }
  if(e && e.target.name=="telNo"){
  if (telNo === "") {
    errors.telNo = "Phone is required"
    invalid = true
  } else if (!telNo.match(/^[0-9]+$/)) {
    errors.telNo = "Numbers only"
    invalid = true
  }
}
  this.setState({
    invalid,
    errors
  })

  return invalid
}

and your FormItem will look like

<FormItem label='First name:' input={{
            type: 'text',
            name: 'firstName',
            value: this.state.firstName,
            onChange: e => this.setState({ firstName: e.target.value }),
            onBlur: (e) => this.validate(e),
          }} error = {this.state.errors.firstName}
        /> 

1 Comment

But in your case, if I change the focus between the input field, the error message is displayed only near the input field with focus and is not displayed next to the two fields, if both have invalid data
1

You may want to consider using redux-form to manage form state across your application. Even if you do not choose to use this library, it is worth taking a look at the meta props that are defined on inputs wrapped by Field's to help clarify what considerations you should make when validating forms.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.