Dead simple React validations with Vest
Tags: testing, javascript, react
Published: 2020-07-11 20:13:38

One of the things that get the most bad rep in web development is form validation. It is something that seems so trivial and a part of almost every app, yet almost always, as your feature gets more complex, your validations seem to [grow in mess and complexity](https://dev.to/ealush/fight-validations-spaghetti-with-vest-3i8j) as well.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/u6ddpgw8k2x14qe0gx28.png)

[Vest](https://github.com/ealush/vest) is a fairly new framework that aims to fix that bad parts in form validations by getting inspired by unit testing framework such as Mocha and Jest. It is framework agnostic, which means that you can use it with any UI framework you currently use.

In this tutorial you will learn how to use the basics of Vest to easily validate forms written in React.

## What we'll be making today?
We'll start with a basic registration form, already implemented in React.

In this tutorial, although not React heavy, I am assuming you are comfortable with React, and have some familiarity with React's useState hook.

The concepts you'll be learning here though, are NOT react specific, and can be applied to any framework.

At any point during this tutorial when you feel that you want to go off-script, feel free to head to the [Vest documentation site](https://ealush.com/vest) to learn about other cool features.

In this form we will add the following validations:
✅ Username required
✅ Username is too short
✅ Password is too short
✅ TOS agreement -> Validation without an error
✅ Passwords do not match -> Cross field validation
✅ Only validating the field that's being changed by the user
✅ Email is invalid
✅ Password is weak -> Warning validation
✅ Disabling the submit button if there are any errors

## Take a look at our finished product:
[Try it live on CodeSandbox](https://codesandbox.io/s/vest-react-tutorial-finished-ztt8t?file=/src/validate.js)

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/z8mt38mmwf37luipfv84.gif)

## Let's start
You can start by forking the [starter sandbox](https://codesandbox.io/s/vest-react-tutorial-starter-4lllq?file=/src/Form.js). It contains a simple React form without any validations at all. All the form does at the moment is update the form state upon input change.

To make things simpler, it also contains an `<Input/>` component that's already designed to show our errors validation errors when we run them later on.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/ojn9w6qql8s5mgi1gj4t.jpg)

### Begin with the basics
The main concept in Vest is that your validation sits separately from your feature. This allows you to easily refactor your feature without touching your validations.

So head over to the `src/validate.js` file. That's where we'll be spending most our time.

The first step is to `import vest from 'vest';`

Next we'll create a new vest suite with our form name and immediately export it.

```js
import vest from 'vest';

export default vest.create(() => {
  
});
```

This export returns a function, that when we run it - it runs our validation.

Now, this function needs to somehow get our form data. We pass it over using our callback function, so let's add a `data` argument to our callback function.

```js
import vest from "vest";

export default vest.create((data = {}) => {
  
});
```

This is the initial structure that's true to every Vest suite.

## The anatomy of a test
As noted before, Vest is inspired by the syntax of unit-testing frameworks, so we write tests in a similar fashion.

A structure of a **jest** test is roughly
```js
test('message', () => {
    expect(value).toBe(anotherValue)
});
```

And the structure of a Vest test is (in this example, `eqauls` is one of many possible assertions).

```js
test('field_name', 'message', () => {
    enforce(value).equals(anotherValue)
})
```

* test - A function that runs a single validation test
* 'field_name' - The field being validated
* 'message' - The error that is presented to the user
* enforce - Vest's assertion library ([docs link](https://ealush.com/vest/#/./enforce?id=list-of-enforce-rules))

## Writing your first test
Our first test is "Username is required", we will check that it is not empty using [`.isNotEmpty()`](https://ealush.com/vest/#/enforce?id=isnotempty).

First, add to your import statement the named imports of `enforce` and `test`.

```js
import vest, { test, enforce } from "vest";
```

and in your suite, add the following test:

```js
  test("username", "Username is required", () => {
    enforce(data.username).isNotEmpty();
  });
```

`validate.js` is now supposed to look like this:

```js
import vest, { test, enforce } from "vest";

export default vest.create((data = {}) => {
  test("username", "Username is required", () => {
    enforce(data.username).isNotEmpty();
  });
});
```

### Connecting Vest to our form
Obviously, this should not work yet. We have done nothing to make our form work with Vest.

Head back to `src/Form.js`.

First, let's import our validation suite into our Form module:

```js
// src/Form.js
import suite from './validate';
```

And now, we need to make our form "aware" of our returned validation results, with `suite.get()`. Whenever we we run our validations, suite.get will contain the most up-to-date validation result.

```js
// src/Form.js
// ...
export default function Form() {
  const [formState, setFormState] = useState({});

  const result = suite.get();
  // ...
}
```

To display the errors on the username field, find it and add it the errors property: `errors={result.getErrors('username')}`

```js
      <Input
        name="username"
        label="Username"
        value={formState.username}
        onChange={handleChange}
        errors={result.getErrors('username')}
      />
```

Vest is now connected to your form, and whenever an error is present - your form will show it. But we did not yet make our form run our validations.

Create a new function called runValidate (the name doesn't matter) in the body of your React component:

This function takes the name of the changed field,
 and its value. It then runs our validation agains the full state.

```js
  const runValidate = (name, value) => {
    const res = validate({
      ...formState,
      ...{ [name]: value }
    });
  };
```

Call runValidate from the `handleChange` handler:

```js
  const handleChange = (name, value) => {
    setFormState({ ...formState, [name]: value });
    runValidate(name, value);
  };
```

Our component is supposed to begin like this:

```js
export default function Form() {
  const [formState, setFormState] = useState({});

  const result = suite.get();

  const runValidate = (name, value) => {
    const res = validate({
      ...formState,
      ...{ [name]: value }
    });
  };

  const handleChange = (name, value) => {
    const nextFormState = { ...formState, [name]: value };
    setFormState(nextFormState);
    runValidate(name, value);
  };

// ...
```

Try typing something into the username field and then remove it. You're supposed to see our error message:
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/n7crk57ysxmuq31bwurv.jpg)


That was easy.

One cool thing about Vest's validation being test-driven, is that you can have multiple tests for the same field, meaning that you can easily display different errors depending on different criteria. For example, if you only want to allow usernames longer than 3 characters, simply add the following test after the first one in `src/validate.js`:

```js
  test("username", "Username is too short", () => {
    enforce(data.username).longerThanOrEquals(3);
  });
```

Now, without any changes to our form our username field should be able to display both messages.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/jse99tvpobyn63m95sfr.gif)


Adding a few more validation tests should be easy. Paste the following into your validation suite in `src/validate.js`. Here are some for our `password` and `confirm_password` inputs.

```js
  test("password", "Password is required", () => {
    enforce(data.password).isNotEmpty();
  });

  test("password", "Password must be at least 6 chars long", () => {
    enforce(data.password).longerThanOrEquals(6);
  });

  test("confirm_password", "Passwords do not match", () => {
    enforce(data.confirm_password).equals(data.password);
  });
```

And in `src/Form.js` add the error property to the password and confirm_password inputs just as like we did with username:

```js
      <Input
        name="password"
        label="Password"
        value={formState.password}
        onChange={handleChange}
        errors={result.getErrors("password")}
      />
      <Input
        name="confirm_password"
        label="Confirm Password"
        value={formState.confirm_password}
        onChange={handleChange}
        errors={result.getErrors("confirm_password")}
      />
```

If you've followed the last steps correctly, you should see the errors appearing for username, password and confirm password when making changes to the form.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/lobwtqxnwb0yh3l3mh66.gif)

The problem?
Because we pass the whole form data to vest, it validates all fields at once, even if the user did not interact with them yet. This is easily fixable.

## Validating just the current field

Ideally, we want to only validate the field the user is currently interacting with, and the fields impacted by it. This is done with `vest.only()` ([docs link](https://ealush.com/vest/#/exclusion)).

To your suite's callback function add a second argument, call it `currentField`, it will hold the field we currently want to validate, and right below it, to your first line of your suite, add `vest.only(currentField);`.

```js
export default vest.create((data = {}, currentField) => {
  vest.only(currentField);
```

This alone won't do anything, because we did not yet tell Vest which field is our current field, so back in `src/Form.js`, modify `runValidate` so that it passes the name of the field as the second argument to `validate`:

```js
  const runValidate = (name, value) => {
    const res = validate({
      ...formState,
      ...{ [name]: value }
    }, name); // -> field name got added here
  };
```

If you try to insert data to your inputs now, you'll see that fields are being validated only as they are interacted with.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/vjb9gmgfxuazo9murckg.gif)

Cool.

## What about styles? 🎨

It's cool that we can validate, but we also want to make UI decisions based on the validation result, for example, painting error fields in red.

Once again, Vest has got you covered. It has a `classNames` ([docs](https://ealush.com/vest/#/utilities?id=classnames-for-simplifying-ui-code))utility function that you can use to generate a class name for your field's validation results.

As a first argument it takes your validation result, and as a second argument, a key value map of a validation state and its output class.

---
In `src/Form.js` add the following import:

```js
import classNames from 'vest/classNames';
```

And in the body of our Form component, add the following assignment:

```js
  const cn = classNames(result, {
    invalid: "invalid",
    valid: "valid"
  });
```

What we basically did here is this:
1. We passed our validation result to `classNames`
2. We told classnames that we would like invalid fields to get the `invalid` class, and valid fields to get the `valid` class. 
3. We stored the output in a const called `cn` for later access. Its value is a function.

The names you use are up to you, but in our tutorial we used these classes in our stylesheet file.

Now, you can go to our `username`, `password` and `confirm_password` inputs, and add a classNames property with a `cn` call with the respective field name as an argument.

```js
      <Input
        name="username"
        label="Username"
        value={formState.username}
        onChange={handleChange}
        errors={result.getErrors("username")}
        className={cn("username")}
      />
      <Input
        name="password"
        label="Password"
        value={formState.password}
        onChange={handleChange}
        errors={result.getErrors("password")}
        className={cn("password")}
      />
      <Input
        name="confirm_password"
        label="Confirm Password"
        value={formState.confirm_password}
        onChange={handleChange}
        errors={result.getErrors("confirm_password")}
        className={cn("confirm_password")}
      />
```

Your inputs should now display the current validity state in color as well.

![Colors](https://dev-to-uploads.s3.amazonaws.com/i/gqniknnsq5e78bzvivda.jpg)

## Validating Terms of Service checkbox

Now that we can display colors, it would be a good time to validate the terms of service checkbox and add a password strength warning.

In `src/validate.js` add:

```js
  test('tos', () => {
    enforce(data.tos).isTruthy()
  })
```

And in `src/Form.js` add to our `tos` Checkbox component the appropriate className:

```js
      <Checkbox
        name="tos"
        checked={formState.tos}
        onChange={handleChange}
        label="I have read and agreed to the terms of service."
        className={cn("tos")}
      />
```

Note that we did not add a failure message to our TOS field because it is already descriptive enough. 

![tos](https://dev-to-uploads.s3.amazonaws.com/i/8td4mwpc9fie0ht8j8tp.gif)

## Adding a password strength warning

Password strength is a sort of a weird creature in the world of validations. It shouts at you, but it still lets you submit the form.

Vest supports non-failing validations with the vest.warn() hook ([docs link](https://ealush.com/vest/#/warn)). It essentially tells Vest to store this field's result in a different basket so you can later treat it differently in your app.

To start, in `src/Form.js` add to our `classNames` call a warning class:

```js
  const cn = classNames(result, {
    warning: "warning",
    invalid: "invalid",
    valid: "valid"
  });
```

And to our validation suite in `src/validate.js` add a simple password strength test.

```js
  test("password", "Password is weak, Maybe add a number?", () => {
    vest.warn(); // using vest.warn marks this test as warning
    enforce(data.password).matches(/[0-9]/);
  });
```

If you test our field now, you'll notice that it appears correctly, but without our warning message.

![Warning without a message](https://dev-to-uploads.s3.amazonaws.com/i/fai9fjaajfavrogqfzvi.jpg)

This is because we only added to our field the error messages for password. As we learned earlier - warnings are stored in a different "basket". We need to go back to `src/Form.js` and tell our password input to also take the warning messages:

```js
      <Input
        name="password"
        label="Password"
        value={formState.password}
        onChange={handleChange}
        errors={[
          ...result.getErrors("password"),
          ...result.getWarnings("password")
        ]}
        className={cn("password")}
      />
```
![Warning](https://dev-to-uploads.s3.amazonaws.com/i/pyzfg38eosmsj60lz1cf.jpg)

## Validating email

We saved email for this late stage because it is a tricky one. Of course, you can test an email Regular expression using `enforce(data.email).matches(SomeEmailRegExp)`, and this is fine.

Another option is to extend enforce with a custom function.

In our example, let's extend enforce with `isEmail` from `validator.js`:

```js
import vest, { test, enforce } from "vest";
import { isEmail } from 'validator';

enforce.extend({ isEmail })
```

and now you can add to your suite the email test:
```js
  test("email", "Email Address is not valid", () => {
    enforce(data.email).isEmail();
  });
```

And for the last time, in `src/Form.js`, add the error messages and classes to our email input:

```js
      <Input
        name="email"
        label="Email"
        value={formState.email}
        onChange={handleChange}
        errors={result.getErrors("email")}
        className={cn("email")}
      />
```

![Email](https://dev-to-uploads.s3.amazonaws.com/i/li2k5kt8t7oftkmmgrb7.jpg)

## Fixing weird confirm behaviour

One thing you may notice when the password is empty and deleting whatever was inside confirm_password, it goes green - which is technically correct, the both password and confirm_password are empty. But this is kinda of wonky.

![When password empty](https://dev-to-uploads.s3.amazonaws.com/i/psw6arnqftlnrjm6s4ny.gif)

This is fairly easy to fix, simply wrap the `confirm_password` test in a condition that it should not be validated unless `password` is present.

```js
  if (data.password) {
    test("confirm_password", "Passwords do not match", () => {
      enforce(data.confirm_password).equals(data.password);
    });
  }
```

Now, whenever the password is empty, `confirm_password` will not be validated at all.

## Handling the submit button

Our very last step is disabling the submit button on error:

Let's add a `disabled` prop to our Button component, with the value of `result.hasErrors()`.

`result.hasErrors()` Is a function that checks if we have any errors in our suite. If we also pass it a field name as an argument, it returns a boolean only for that field.

```js
<Button disabled={result.hasErrors()}>Submit</Button>
```

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/j7skk944v0re2acxdjze.gif)

### Wrapping up.

That's it! I hope you have enjoyed this tutorial of Vest.
You can see the even with little effort you can write pretty complex form validations that's still easy to read and maintain.

You can carry on by yourself using Vest's [documentation site](https://ealush.com/vest), contribute to the on project on [github](https://github.com/ealush/vest), or hit me directly on [twitter](https://twitter.com/AlushEvyatar)