React Form Validation
Tags: react, javascript, forms, validation
Published: 2020-04-10 15:26:20

I personally use to find forms difficult. A lot of forms I have worked with would have fields that'd collect different types of data, perform different types of validation, and I never really had a structure or strategy to work with forms in such a way that I liked. A lot of the components in the forms, such as a text field, date picker,  or specialized text fields that'd collect currency data or had requirements around the type of data entered (i.e. integers only, such as a zip code), would sometimes be hard for me to understand due to the varying considerations. I'd also feel my form components bloat significantly.

React to me (pre-hooks) really encouraged me to think more in an object-oriented way, which helped me personally to design forms and components that I liked more than previous libraries or patterns I used. It eventually felt a lot easier to visualize ways of managing forms, in terms of keeping the code  [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself) as possible, and also empowered me with ways to think about how I could have a relatively consistent interface for all of my input components no matter the data.

With that, this post will go into creating a form that will have 8 fields with 6 different types of validation, and an optional field. The form has a date picker, some text fields, an integer-only fixed-length field, and a currency field. 

If you would like to access the code in this repo prior to reading the post, check the code out [here](https://github.com/AlejandroLuperon/reactformvalidation). 😃😃😃

![localhost_3000_sign-up-form-with-urlemail (4).png](https://cdn.hashnode.com/res/hashnode/image/upload/v1586125443316/u0DtO0qEs.png)

The following fields below will be in the form along with the validations that will be performed.

- First Name: Cannot be empty.
- Last Name: Cannot be empty.
- E-mail: Cannot be empty.
- Start Date: Cannot be empty (I'll use [react-datetime](https://www.npmjs.com/package/react-datetime), but this can be changed out to your own component or a library of your choosing). 
- Personal URL (Optional): An optional field, that, if filled, must be a valid URL.
- Salary: Cannot be empty. Must be a valid currency (using  [react-currency-format](https://www.npmjs.com/package/react-currency-format)) to collect currency information.
- Occupation: Cannot be empty.
- Zipcode: Cannot be empty. Is only going to allow for integer inputs.

I will start with a react component, with each field being represented in state with an object:

```javascript 
import React, { Component } from 'react';

class SignUpForm extends Component {
  constructor(props) {
    super(props);
    this.state = {
      firstName: {
        value: "",
        error: false
      },
      lastName: {
        value: "",
        error: false
      },
      email: {
        value: "",
        error: false
      },
      startdate: {
        value: "",
        error: false
      },
      url: {
        value: "",
        error: false
      },
      salary: {
        value: "",
        quantity: "",
        error: false,
        errorMessage: "Salary cannot be empty."
      },
      occupation: {
        value: "",
        error: false
      },
      zipcode: {
        value: "",
        error: false
      }
    }
  }
  render() {
    return (null)
  }
}

export default SignUpForm;
``` 

Each object in state for each field has at least two values, `value` and `error`, set to false. The reason for this is because the form will eventually control the error state of the children field components. The `salary` key in state also has two additional properties, `quantity`, and `errorMessage`. The `CurrencyFormat` component we use comes from `react-currency-format`, and will return two values  when the `onChange` event is triggered, a value that represents the formatted currency amount (i.e. '$60,000'), and an unformatted currency amount (i.e. '60000'). 

The name of each key in state is going to be the name of each 'input' inside our form. So that way, whenever our various `onChange` events are called, we can update the appropriate key in state.

Next, we'll add the fields to our render method that only collect text, 'First Name', 'Last Name', and 'Occupation', along with the corresponding `onChange` method.

```javascript 
import React, { Component } from 'react';
import TextField from 'components/TextField.component';
import './signupform.css';

class SignUpForm extends Component {
  constructor(props) {
    super(props);
    this.state = {
      firstName: {
        value: "",
        error: false
      },
      lastName: {
        value: "",
        error: false
      },
      email: {
        value: "",
        error: false
      },
      startdate: {
        value: "",
        error: false
      },
      url: {
        value: "",
        error: false
      },
      salary: {
        value: "",
        quantity: "",
        error: false,
        errorMessage: "Salary cannot be empty."
      },
      occupation: {
        value: "",
        error: false
      },
      zipcode: {
        value: "",
        error: false
      }
    }
  }

  onChange(event) {
    this.setState({ [event.target.name]: {
        ...this.state[event.target.name],
        value: event.target.value,
        error: this.state[event.target.name].error
      }
    })
  }

  render() {
    let {
      firstName,
      lastName,
      occupation
    } = this.state;

    return(
      <div className="container">
        <form className="signup-form">
          <TextField
            error={firstName.error}
            value={firstName.value}
            name="firstName"
            errorMessage="First name cannot be empty."
            label="First Name"
            placeholder="First Name"
            onChange={this.onChange.bind(this)} />
          <TextField
            error={lastName.error}
            value={lastName.value}
            name="lastName"
            errorMessage="Last name cannot be empty."
            label="Last Name"
            placeholder="Last Name"
            onChange={this.onChange.bind(this)} />
          <TextField
            error={occupation.error}
            value={occupation.value}
            name="occupation"
            errorMessage="Occupation cannot be empty."
            label="Occupation"
            placeholder="Occupation"
            onChange={this.onChange.bind(this)} />
        </form>
      </div>
    )
  }
}

export default SignUpForm;
``` 

![localhost_3000_sign-up-form-text-only.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1586111959048/A_tvMZr3P.png)

If we look at the onChange event:

```javascript
  onChange(event) {
    let { name, value } = event.target;
    this.setState({ [name]: {
        ...this.state[name],
        value: event.target.value
      }
    })
  }
```

The `event` object from the `TextField` gets passed to the `onChange` function in our `SignUpForm`, where the 'name' and 'value' properties get extracted from event.target. 

These values are used to update state for the key indicated by name. If the TextField's name is not represented as a key in state, you may get an undefined error when the `onChange` event is triggered on that field.

Looking at our TextField component:

```javascript
import React, { Component } from 'react';
import './TextField.css';

class TextField extends Component {
  static defaultProps = {
    size: "large"
  }

  setSize() {
    let { size } = this.props;
    if (size === 'large') return 'textfield-large';
  }

  render() {
    let {
      name,
      value,
      placeholder,
      label,
      errorMessage,
      error,
      onChange
    } = this.props;

    return(
      <div className={`d-flex flex-column ${this.setSize()}`}>
        <label className="form-item-label">{label}</label>
        <input
          className={`textfield regular-text ${this.setSize()}`}
          value={value}
          name={name}
          placeholder={placeholder}
          onChange={onChange.bind(this)} />
        {(error ? <div className="error-text-container"><div className="error-text form-item-error">{errorMessage}</div></div> : null)}
      </div>
    )
  }
}

export default TextField;
```

The `TextField` component accepts several properties. The prop `label` sets the label of the TextField, `onChange` is a property that is passed from the parent component, and `value` is passed down from the parent component as well. Whenever the input's `onChange` event is triggered, the parent manages the update and passes the new value down as a prop. The property `errorMessage` is shown when the property `error` is true.

If a validation is triggered and failed in the parent, the TextField's `error` property will be set to true. 

At this point, instead of continuing to add additional fields to the form, I like to trigger the form submission and see if my currently added fields are working as expected, so let's do that below:

```javascript
import React, { Component } from 'react';
import TextField from 'components/TextField.component';
import Button from 'components/Button.component';
import {
  isFilled
} from 'common/helpers/validators';
import './signupform.css';

class SignUpForm extends Component {
  constructor(props) {
    super(props);
    this.state = {
      firstName: {
        value: "",
        error: false
      },
      lastName: {
        value: "",
        error: false
      },
      email: {
        value: "",
        error: false
      },
      startdate: {
        value: "",
        error: false
      },
      url: {
        value: "",
        error: false
      },
      salary: {
        value: "",
        quantity: "",
        error: false,
        errorMessage: "Salary cannot be empty."
      },
      occupation: {
        value: "",
        error: false
      },
      zipcode: {
        value: "",
        error: false
      },
      result: ""
    }
  }

  onChange(event) {
    let { name, value } = event.target;
    this.setState({ [name]: {
        ...this.state[name],
        value: event.target.value
      }
    })
  }

  isFieldValid(validator, key) {
    let isValid = validator(this.state[key].value);

    this.setState({
      [key]: {
        value: this.state[key].value,
        error: !isValid,
        errorMessage: this.state[key].errorMessage
      }
    });

    return isValid;
  }

  submit(event) {
    event.preventDefault();
    if (!this.validate()) {
      return;
    }
    let { firstName, lastName, occupation } = this.state;
    this.setState({result: `Success! First Name: ${firstName.value}, Last Name: ${lastName.value}, Occupation: ${occupation.value}`});
  }

  validate() {
    let fields = new Set();
    fields.add(this.isFieldValid(isFilled, "firstName"));
    fields.add(this.isFieldValid(isFilled, "lastName"));
    fields.add(this.isFieldValid(isFilled, "occupation"));

    return !fields.has(false);
  }

  render() {
    let {
      firstName,
      lastName,
      occupation
    } = this.state;

    return(
      <div className="container">
        <form onSubmit={this.submit.bind(this)} className="signup-form">
          <TextField
            error={firstName.error}
            value={firstName.value}
            name="firstName"
            errorMessage="First name cannot be empty."
            label="First Name"
            placeholder="First Name"
            onChange={this.onChange.bind(this)} />
          <TextField
            error={lastName.error}
            value={lastName.value}
            name="lastName"
            errorMessage="Last name cannot be empty."
            label="Last Name"
            placeholder="Last Name"
            onChange={this.onChange.bind(this)} />
          <TextField
            error={occupation.error}
            value={occupation.value}
            name="occupation"
            errorMessage="Occupation cannot be empty."
            label="Occupation"
            placeholder="Occupation"
            onChange={this.onChange.bind(this)} />
          <Button
            style={{marginTop: '25px'}}
            type="submit"
            label="Submit"/>
        </form>
        <div style={{marginTop: '25px'}}>{this.state.result}</div>
      </div>
    )
  }
}

export default SignUpForm;
```

There were three functions added in addition to the `Submit` button: `submit`, `validate`, and `isFieldValid`. Let's first look at `submit`:

```javascript
  submit(event) {
    event.preventDefault();
    if (!this.validate()) {
      return;
    }
    let { firstName, lastName, occupation } = this.state;
    this.setState({result: `Success! First Name: ${firstName.value}, Last Name: ${lastName.value}, Occupation: ${occupation.value}`});
  }
```

We call `event.preventDefault()` to stop the page from refreshing when the submit event is triggered on the form. After that, we have an `if` statement, that exits the function if our `validate` function returns false.
If our `validate` function returns `true`, then the values of the fields in our form is printed just below the form, just to temporarily test our expected behavior.

Our `validate` function runs validations on all of our fields in our form:

```javascript
  validate() {
    let fields = new Set();
    fields.add(this.isFieldValid(isFilled, "firstName"));
    fields.add(this.isFieldValid(isFilled, "lastName"));
    fields.add(this.isFieldValid(isFilled, "occupation"));

    return !fields.has(false);
  }
```

If a field does not contain valid data, then a `false` is added to the set, and thus `validate` returns false. Our function `isFieldValid` takes two arguments, a function to validate the data of the field, and the second argument is the key in state that the field corresponds to. That key is used to retrieve the field's value and pass it to it's validator:

```javascript
  isFieldValid(validator, key) {
    let isValid = validator(this.state[key].value);

    this.setState({
      [key]: {
        value: this.state[key].value,
        error: !isValid,
        errorMessage: this.state[key].errorMessage
      }
    });
```
We import a function `isFilled`, which checks to see if the item passed is empty. To do that, I have used a function from [validator](https://www.npmjs.com/package/validator). We didn't have to use validator, I just elected to use it for ease and convenience, but if you don't want to include another package, we can also replace the code within the `isFilled` function with your own logic.

```javascript
export const isFilled = (value) => {
  return !validator.isEmpty(value + "") && value !== null;
}
```

At this point, if we click submit on an empty form, we will see:

![localhost_3000_sign-up-form-with-button.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1586113758297/B7owckwMa.png)


If we add data and click the submit button, we will see the values we added to the fields in our form:


![localhost_3000_sign-up-form-with-button (1).png](https://cdn.hashnode.com/res/hashnode/image/upload/v1586114782145/G6MPUVSDF.png)

Next, we'll add our `url` and `email` fields, and make corresponding updates to the `validate` function to check the validity of these two fields. `url` is an optional field, but if it is not empty, it needs to be a valid URL.

```javascript 
import React, { Component } from 'react';
import TextField from 'components/TextField.component';
import Button from 'components/Button.component';
import {
  isFilled,
  isEmailValid,
  isURLValid,
} from 'common/helpers/validators';
import './signupform.css';

class SignUpForm extends Component {
  constructor(props) {
    super(props);
    this.state = {
      firstName: {
        value: "",
        error: false
      },
      lastName: {
        value: "",
        error: false
      },
      email: {
        value: "",
        error: false
      },
      startdate: {
        value: "",
        error: false
      },
      url: {
        value: "",
        error: false
      },
      salary: {
        value: "",
        quantity: "",
        error: false,
        errorMessage: "Salary cannot be empty."
      },
      occupation: {
        value: "",
        error: false
      },
      zipcode: {
        value: "",
        error: false
      },
      result: ""
    }
  }

  onChange(event) {
    let { name, value } = event.target;
    this.setState({ [name]: {
        ...this.state[name],
        value: event.target.value
      }
    })
  }

  isFieldValid(validator, key) {
    let isValid = validator(this.state[key].value);

    this.setState({
      [key]: {
        value: this.state[key].value,
        error: !isValid,
        errorMessage: this.state[key].errorMessage
      }
    });

    return isValid;
  }

  submit(event) {
    event.preventDefault();
    if (!this.validate()) {
      return;
    }
    let { firstName, lastName, occupation } = this.state;
    this.setState({result: `Success! First Name: ${firstName.value}, Last Name: ${lastName.value}, Occupation: ${occupation.value}`});
  }

  validate() {
    let fields = new Set();
    fields.add(this.isFieldValid(isFilled, "firstName"));
    fields.add(this.isFieldValid(isFilled, "lastName"));
    fields.add(this.isFieldValid(isFilled, "occupation"));
    fields.add(this.isFieldValid(isEmailValid, "email"))
    fields.add(this.isPersonalURLValid());

    return !fields.has(false);
  }

  isPersonalURLValid() {
    let { value } = this.state.url;

    let isValid = isURLValid(value) || value.length === 0;

    this.setState({
      url: {
        ...this.state.url,
        error: !isValid
      }
    });

    return isValid;
  }

  render() {
    let {
      firstName,
      lastName,
      occupation,
      url,
      email
    } = this.state;

    return(
      <div className="container">
        <form onSubmit={this.submit.bind(this)} className="signup-form">
          <TextField
            error={firstName.error}
            value={firstName.value}
            name="firstName"
            errorMessage="First name cannot be empty."
            label="First Name"
            placeholder="First Name"
            onChange={this.onChange.bind(this)} />
          <TextField
            error={lastName.error}
            value={lastName.value}
            name="lastName"
            errorMessage="Last name cannot be empty."
            label="Last Name"
            placeholder="Last Name"
            onChange={this.onChange.bind(this)} />
          <TextField
            error={occupation.error}
            value={occupation.value}
            name="occupation"
            errorMessage="Occupation cannot be empty."
            label="Occupation"
            placeholder="Occupation"
            onChange={this.onChange.bind(this)} />
          <TextField
            error={url.error}
            value={url.value}
            name="url"
            errorMessage="Please enter a vaild url."
            label="Personal Website (Optional)"
            placeholder="Personal Website"
            onChange={this.onChange.bind(this)} />
          <TextField
            error={email.error}
            value={email.value}
            name="email"
            errorMessage="Please enter a vaild e-mail."
            label="E-mail"
            placeholder="E-mail"
            onChange={this.onChange.bind(this)} />
          <Button
            style={{marginTop: '25px'}}
            type="submit"
            label="Submit"/>
        </form>
        <div style={{marginTop: '25px'}}>{this.state.result}</div>
      </div>
    )
  }
}

export default SignUpForm;
```

![localhost_3000_sign-up-form-with-urlemail.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1586122290131/MOH0OLCwN.png)

We've updated the `validate` function:

```javascript
  validate() {
    let fields = new Set();
    fields.add(this.isFieldValid(isFilled, "firstName"));
    fields.add(this.isFieldValid(isFilled, "lastName"));
    fields.add(this.isFieldValid(isFilled, "occupation"));
    fields.add(this.isFieldValid(isEmailValid, "email"))
    fields.add(this.isPersonalURLValid());

    return !fields.has(false);
  }
```

To check for e-mail validity, I have again used a function from the validator library like so:

```javascript
export const isEmailValid = (email) => {
  return validator.isEmail(email)
}
```

There is another new function also called when `validate` is called, and that is `isPersonalURLValid`. Because 'Personal Website' is an optional field, it's okay for it to be empty, it just has to be a valid URL if not. Our function looks like: 

```javascript
  isPersonalURLValid() {
    let { value } = this.state.url;

    let isValid = isURLValid(value) || value.length === 0;

    this.setState({
      url: {
        ...this.state.url,
        error: !isValid
      }
    });

    return isValid;
  }
```

This function checks to see if the `url` value is either an empty string or a valid URL using our `isURLValid` function, again leaning on the `validator` library to provide a function to check validity: 

```javascript
export const isURLValid = (url) => {
  return validator.isURL(url);
}
```
With these fields added, our form now looks like this whenever submit is triggered and no data is entered:

![localhost_3000_sign-up-form-with-urlemail (1).png](https://cdn.hashnode.com/res/hashnode/image/upload/v1586122734084/i0RX1uwA6.png)

If data is entered into the 'Personal Website' text field, our form looks like:

![localhost_3000_sign-up-form-with-urlemail (2).png](https://cdn.hashnode.com/res/hashnode/image/upload/v1586122773177/ivpXS9CPQ.png)

We have three fields left, our 'Desired Start Date', 'Zipcode' and 'Desired Salary' fields, so let's add them:

```javascript
import React, { Component } from 'react';
import TextField from 'components/TextField.component';
import Button from 'components/Button.component';
import DatePicker from 'components/DatePicker.component';
import CurrencyFormat from 'react-currency-format';
import {
  isFilled,
  isEmailValid,
  isURLValid,
  isLengthValid
} from 'common/helpers/validators';
import './signupform.css';

class SignUpForm extends Component {
  constructor(props) {
    super(props);
    this.state = {
      firstName: {
        value: "",
        error: false
      },
      lastName: {
        value: "",
        error: false
      },
      email: {
        value: "",
        error: false
      },
      startdate: {
        value: "",
        error: false
      },
      url: {
        value: "",
        error: false
      },
      salary: {
        value: "",
        quantity: "",
        error: false,
        errorMessage: "Salary cannot be empty."
      },
      occupation: {
        value: "",
        error: false
      },
      zipcode: {
        value: "",
        error: false
      },
      result: ""
    }
  }

  onChange(event) {
    let { name, value } = event.target;
    this.setState({ [name]: {
        ...this.state[name],
        value: event.target.value
      }
    })
  }

  isFieldValid(validator, key) {
    let isValid = validator(this.state[key].value);

    this.setState({
      [key]: {
        value: this.state[key].value,
        error: !isValid,
        errorMessage: this.state[key].errorMessage
      }
    });

    return isValid;
  }

  submit(event) {
    event.preventDefault();
    if (!this.validate()) {
      return;
    }
    let { firstName, lastName, occupation, email, url, salary, startdate, zipcode } = this.state;
    this.setState({result: `Success! First Name: ${firstName.value}, Last Name: ${lastName.value}, Occupation: ${occupation.value}, Email: ${email.value}, URL: ${url.value}, Zipcode: ${zipcode.value}, Desired Start Date: ${startdate.value}, Desired Salary: ${salary.value}`});
  }

  validate() {
    let fields = new Set();
    fields.add(this.isFieldValid(isFilled, "firstName"));
    fields.add(this.isFieldValid(isFilled, "lastName"));
    fields.add(this.isFieldValid(isFilled, "occupation"));
    fields.add(this.isFieldValid(isEmailValid, "email"))
    fields.add(this.isPersonalURLValid());
    fields.add(this.isFieldValid(isFilled, "startdate"));
    fields.add(this.isSalaryMin(min, 60000, "salary", "Minimum salary is $60,000."));
    fields.add(this.isZipcodeValid());

    return !fields.has(false);
  }

  isPersonalURLValid() {
    let { value } = this.state.url;

    let isValid = isURLValid(value) || value.length === 0;

    this.setState({
      url: {
        ...this.state.url,
        error: !isValid
      }
    });

    return isValid;
  }


  isSalaryMin(validator, value, key, errorMessage) {
    this.setState({
      [key]: {
        quantity: this.state[key].quantity,
        value: this.state[key].value,
        error: !validator(this.state[key].quantity, value),
        errorMessage: errorMessage
      }
    });

    return validator(this.state[key].quantity, value);
  }

  onValueChange(values) {
    const {formattedValue, value} = values;
    this.setState({ salary: {
        ...this.state.salary,
        value: formattedValue,
        quantity: value,
      }
    });
  }

  onChangeDate(key, value) {
    this.setState({
      [key]: {
        value,
        error: false
      },
    });
  }

  onChangeZipcode(event) {
    let { value } = event.target

    if (value.length > 5) return;

    let quantity = 0;

    let OK = /[0-9+$]/.test(value)

    if (!OK && value.length > 0) return;

    if (value.length > 0) {
      value = parseInt(value);
    } else {
      value = "";
    }

    this.setState({ zipcode: {
        value
      }
    })
  }

  isZipcodeValid() {
    let value = this.state.zipcode.value.toString();
    let isValid = isLengthValid(value, 5);
    let errorMessage = "Zipcode cannot be empty.";

    if (!isValid && value.length > 0) {
      errorMessage = "Zipcode must be five digits.";
    }

    this.setState({
      zipcode: {
        ...this.state.zipcode,
        error: !isValid,
        errorMessage
      }
    });
    return isValid;
  }

  render() {
    let {
      firstName,
      lastName,
      occupation,
      url,
      email,
      startdate,
      salary,
      zipcode
    } = this.state;

    return(
      <div className="container">
        <form onSubmit={this.submit.bind(this)} className="signup-form">
          <TextField
            error={firstName.error}
            value={firstName.value}
            name="firstName"
            errorMessage="First name cannot be empty."
            label="First Name"
            placeholder="First Name"
            onChange={this.onChange.bind(this)} />
          <TextField
            error={lastName.error}
            value={lastName.value}
            name="lastName"
            errorMessage="Last name cannot be empty."
            label="Last Name"
            placeholder="Last Name"
            onChange={this.onChange.bind(this)} />
          <TextField
            error={occupation.error}
            value={occupation.value}
            name="occupation"
            errorMessage="Occupation cannot be empty."
            label="Occupation"
            placeholder="Occupation"
            onChange={this.onChange.bind(this)} />
          <TextField
            error={url.error}
            value={url.value}
            name="url"
            errorMessage="Please enter a vaild url."
            label="Personal Website (Optional)"
            placeholder="Personal Website"
            onChange={this.onChange.bind(this)} />
          <TextField
            error={email.error}
            value={email.value}
            name="email"
            errorMessage="Please enter a vaild e-mail."
            label="E-mail"
            placeholder="E-mail"
            onChange={this.onChange.bind(this)} />
          <DatePicker
            timeFormat={false}
            isValidDate={(current) => current > new Date()}
            value={(startdate.value ? new Date(startdate.value) : null)}
            placeholder="Desired Start Date"
            errorMessage="Desired start date cannot be empty."
            error={startdate.error}
            onChange={this.onChangeDate.bind(this, "startdate")}
            label="Desired Start Date"
            size="large"/>
          <CurrencyFormat
            thousandSeparator={true}
            prefix='$'
            customInput={TextField}
            name="salary"
            value={salary.quantity}
            error={salary.error}
            errorMessage={salary.errorMessage}
            label="Desired Salary - Min. $60,000"
            placeholder='Desired Salary'
            onValueChange={this.onValueChange.bind(this)} />
          <TextField
            name="zipcode"
            label="Zipcode"
            error={zipcode.error}
            value={zipcode.value}
            errorMessage={zipcode.errorMessage}
            placeholder="Zipcode"
            onChange={this.onChangeZipcode.bind(this)} />
          <Button
            style={{marginTop: '25px'}}
            type="submit"
            label="Submit"/>
        </form>
        <div style={{marginTop: '25px'}}>{this.state.result}</div>
      </div>
    )
  }
}

export default SignUpForm;
```

We've added another five functions for the three added fields. Starting with `zipcode`, we've added another validate function, `isZipcodeValid`, and `onChangeZipcode`. The 'Zipcode' field is 5 digits, and can only contain integers, thus, our `onChangeZipcode` function will disallow non-integer characters, and limit the length of the value to 5:

```javascript
  onChangeZipcode(event) {
    let { value } = event.target

    if (value.length > 5) return;

    let OK = /[0-9+$]/.test(value)

    if (!OK && value.length > 0) return;

    if (value.length > 0) {
      value = parseInt(value);
    } else {
      value = "";
    }

    this.setState({ zipcode: {
        value
      }
    })
  }
```

For this field, I use a regular expression to check for validity, but you can use any library or method you prefer.

For the function to check the inputs validity, we check for the fields length:

```javascript
  isZipcodeValid() {
    let value = this.state.zipcode.value.toString();
    let isValid = isLengthValid(value, 5);
    let errorMessage = "Zipcode cannot be empty.";

    if (!isValid && value.length > 0) {
      errorMessage = "Zipcode must be five digits.";
    }

    this.setState({
      zipcode: {
        ...this.state.zipcode,
        error: !isValid,
        errorMessage
      }
    });
    return isValid;
  }
```

If the value for `zipcode` is greater than 0 in length, but less than 5, then the `errorMessage` for zipcode becomes  "Zipcode must be five digits.". If the field is empty, then the `errorMessage` is: "Zipcode cannot be empty."

This example is to illustrate an instance in which a field could have multiple possible reasons of failure that you may want to communicate to the user.

Our next field that we'll look at is 'Desired Start Date'. We've added an `onChange` function specific to this field:

```javascript
  onChangeDate(key, value) {
    this.setState({
      [key]: {
        value,
        error: false
      },
    });
  }
```

Our `DatePicker` component itself looks like:

```javascript
import Datetime from 'react-datetime'
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import moment from 'moment';
import './DatePicker.css';

const dateFormat = 'MM/DD/YYYY';

class DatePicker extends Component {
  static propTypes = {
    label: PropTypes.string,
    size: 'large'
  }

  constructor(props) {
    super(props);
    this.state = {
      value: props.value,
      active: false,
      date: null,
      focused: false
    };
  }

  setSize() {
    let { size } = this.props;
    if (size === 'large') return 'textfield-large';
  }

  getBase() {
    let { timeFormat, value, onChange, placeholder, isValidDate } = this.props;
    let className = `textfield regular-text ${this.setSize()}`;
    return (<Datetime
      isValidDate={isValidDate}
      timeFormat={timeFormat}
      dateFormat="YYYY-MM-DD"
      placeholder={placeholder}
      value={(value ? moment(value, dateFormat) : null)}
      onChange={onChange.bind(this)}
      inputProps={{ className, readOnly: true, placeholder: `${placeholder}`}} />);
  }

  render() {
    let { size, error, errorMessage } = this.props;

    return (
      <div className={`d-flex flex-column ${this.setSize()}`}>
        <label className="form-item-label">{this.props.label}</label>
        {this.getBase()}
        {(error ? <div className="error-text-container"><div className="error-text form-item-error">{errorMessage}</div></div> : null)}
      </div>
    );
  }
}

export default DatePicker;
```

And, in our form, the instance of our `DatePicker` component in our form looks like:

```javascript
          <DatePicker
            timeFormat={false}
            isValidDate={(current) => current > new Date()}
            value={(startdate.value ? new Date(startdate.value) : null)}
            placeholder="Desired Start Date"
            errorMessage="Desired start date cannot be empty."
            error={startdate.error}
            onChange={this.onChangeDate.bind(this, "startdate")}
            label="Desired Start Date"
            size="large"/>
```

I personally find `react-datetime` to be very friendly, as you can set a date range, pass a css class to the `DatePicker` which will style it to your liking, and specify a time option as well, if interested. But, you can use any `DatePicker` of your choice.

Our last field is our `salary` field:

```javascript
          <CurrencyFormat
            thousandSeparator={true}
            prefix='$'
            customInput={TextField}
            name="salary"
            value={salary.quantity}
            error={salary.error}
            errorMessage={salary.errorMessage}
            label="Desired Salary - Min. $60,000"
            placeholder='Desired Salary'
            onValueChange={this.onValueChange.bind(this)} />
```

 We've added two functions for this field, 'isSalaryMin' and 'onValueChange'. 

The `CurrencyFormat` component from  [react-currency-format](https://www.npmjs.com/package/react-currency-format) I also find to be easy to use. I didn't want to spend time to make my own currency component, so I used this library and integrated it to work with my form.

`CurrencyFormat` is great because you can pass a base component to the `customInput` prop, which will effectively wrap the `CurrencyFormat` magic around the passed component. You can still access the props of your base component as well.

`onChange` of the currency input, two values are returned, the formatted currency amount, and the unformatted currency amount. We have a custom function `onValueChange` to grab those two values and set them in state for the salary key:

```javascript
  onValueChange(values) {
    const {formattedValue, value} = values;
    this.setState({ salary: {
        ...this.state.salary,
        value: formattedValue,
        quantity: value,
      }
    });
  }
```

The function we use to validate the value of the salary amount, `isSalaryMin`, is invoked in the `validate` function. It's had a couple updates, so let's take a closer look:

```javascript
  validate() {
    let fields = new Set();
    fields.add(this.isFieldValid(isFilled, "firstName"));
    fields.add(this.isFieldValid(isFilled, "lastName"));
    fields.add(this.isFieldValid(isFilled, "occupation"));
    fields.add(this.isFieldValid(isEmailValid, "email"))
    fields.add(this.isPersonalURLValid());
    fields.add(this.isFieldValid(isFilled, "startdate"));
    fields.add(this.isSalaryMin(min, 60000, "salary", "Minimum salary is $60,000."));
    fields.add(this.isZipcodeValid());

    return !fields.has(false);
  }
```

The `isSalaryMin` function is passed a validator, the minimum salary of $60,000 in integer form, and an error message that is set on error. 

I added these for arguments to the `isSalaryMin` function because I thought about also adding an upper bound to salary and naming the function `isSalaryValid` instead, so that way I could reuse the function for both the lower and upper bound validation. It'd allow me to pass a different validator function and other arguments, but for the purposes of this example, there is no upper bound.

We see that our other new fields added are validated. The `isZipcodeValid` function is called, and we also check to see if a date has been selected.

Finally, looking at our `isSalaryMin` function, we have:

```javascript

  isSalaryMin(validator, value, key, errorMessage) {
    this.setState({
      [key]: {
        quantity: this.state[key].quantity,
        value: this.state[key].value,
        error: !validator(this.state[key].quantity, value),
        errorMessage: errorMessage
      }
    });

    return validator(this.state[key].quantity, value);
  }

```

With our form complete, our form looks like:

![localhost_3000_sign-up-form-with-urlemail (4).png](https://cdn.hashnode.com/res/hashnode/image/upload/v1586125543852/N6xaJaQt8.png)

With this approach, I am able to share my validators across my entire app, I can easily change out the guts of my components at any time, and the parent form component is in charge of validation. 

My form component is under 300 lines of code, and I am definitely certain there is probably opportunity to make the form smaller and more lean or easier to understand.

Please check out a repo with the final code [here](https://github.com/AlejandroLuperon/blogposts/tree/master/reactformvalidation). 😃😃😃