Form validation in React using the useReducer Hook
Tags: react, hooks
Published: 2021-04-27 03:20:14

# Form Validation Library Comparison

There are a lot of libraries out there for validating forms in react.
[Redux-Form](https://redux-form.com/), [Formik](https://formik.org/), [react-final-form](https://final-form.org/react) are few among them.

While these libraries are cool and they help in validating the forms to a great extent, they come with a catch: they add up to bundle size.
Let's see a quick comparison between these libraries:

## Redux Form

Redux form cannot function on its own.
It has 2 additional dependencies [redux](https://www.npmjs.com/package/redux) and [react-redux](https://www.npmjs.com/package/react-redux).
If you are already using redux in your application, then you have already installed redux and react-redux packages.
You can see that from the [bundle phobia](https://bundlephobia.com/) analysis given below that it adds 35 kB to your bundle size, while react itself is just about 38.5 kB.

![redux form stats](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8xsbgsnye2hdyxd6mdxo.JPG)

## Formik

Formik can function on its own without any additional packages to be installed along with it.
The bundle size is 15 kB, which is considerably smaller than that of redux-form.

![formik stats](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z88a34b61y6ho7tfuupf.JPG)

## React Final Form

React final form is created by the author ([@erikras](https://twitter.com/erikras)) of redux-form.
It is a wrapper around the [final-form](https://final-form.org/) core, which has no dependencies.
Since one of the goals behind react final forms was to reduce bundle size, it weighs 8.5 kB gzipped.

![react final form](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6lm7panbo3w4g78g5dkf.JPG)

Now let's see how we can do form validation without depending upon these libraries:

# Setting up the project

Create a new react project using the following command:

```bash
npx create-react-app react-form-validation
```

Update `App.js` with the following code:

```jsx
import React from "react"
import "./App.css"

function App() {
  return (
    <div className="App">
      <h1 className="title">Sign Up</h1>
      <form>
        <div className="input_wrapper">
          <label htmlFor="name">Name:</label>
          <input type="text" name="name" id="name" />
        </div>
        <div className="input_wrapper">
          <label htmlFor="email">Email:</label>
          <input type="email" name="email" id="email" />
        </div>
        <div className="input_wrapper">
          <label htmlFor="password">Password:</label>
          <input type="password" name="password" id="password" />
        </div>
        <div className="input_wrapper">
          <label htmlFor="mobile">Mobile:</label>
          <input type="text" name="mobile" id="mobile" />
        </div>
        <div className="input_wrapper">
          <label className="toc">
            <input type="checkbox" name="terms" /> Accept terms and conditions
          </label>
        </div>
        <div className="input_wrapper">
          <input className="submit_btn" type="submit" value="Sign Up" />
        </div>
      </form>
    </div>
  )
}

export default App
```

Here we have created a simple sign up form with few fields. Now to style these fields let's add some css to `App.css`:

```css
.App {
  max-width: 300px;
  margin: 1rem auto;
}
.title {
  text-align: center;
}
.input_wrapper {
  display: flex;
  flex-direction: column;
  margin-bottom: 0.5rem;
}
.input_wrapper label {
  font-size: 1.1rem;
}
.input_wrapper input {
  margin-top: 0.4rem;
  font-size: 1.1rem;
}
.submit_btn {
  cursor: pointer;
  padding: 0.2rem;
}
.toc,
.toc input {
  cursor: pointer;
}
```

Now if you open the app, you should see our basic form set up:

![basic form](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6lnfhl9hh1sylmzdccpz.JPG)

## Binding the form value with the state

Now that we have the form ready, let's bind the input values with the state

```jsx
import React, { useReducer } from "react"
import "./App.css"

/**
 * The initial state of the form
 * value: stores the value of the input field
 * touched: indicates whether the user has tried to input anything in the field
 * hasError: determines whether the field has error.
 *           Defaulted to true since all fields are mandatory and are empty on page load.
 * error: stores the error message
 * isFormValid: Stores the validity of the form at any given time.
 */
const initialState = {
  name: { value: "", touched: false, hasError: true, error: "" },
  email: { value: "", touched: false, hasError: true, error: "" },
  password: { value: "", touched: false, hasError: true, error: "" },
  mobile: { value: "", touched: false, hasError: true, error: "" },
  terms: { value: false, touched: false, hasError: true, error: "" },
  isFormValid: false,
}

/**
 * Reducer which will perform form state update
 */
const formsReducer = (state, action) => {
  return state
}

function App() {
  const [formState, dispatch] = useReducer(formsReducer, initialState)

  return (
    <div className="App">
      <h1 className="title">Sign Up</h1>
      <form>
        <div className="input_wrapper">
          <label htmlFor="name">Name:</label>
          <input
            type="text"
            name="name"
            id="name"
            value={formState.name.value}
          />
        </div>
        <div className="input_wrapper">
          <label htmlFor="email">Email:</label>
          <input
            type="email"
            name="email"
            id="email"
            value={formState.email.value}
          />
        </div>
        <div className="input_wrapper">
          <label htmlFor="password">Password:</label>
          <input
            type="password"
            name="password"
            id="password"
            value={formState.password.value}
          />
        </div>
        <div className="input_wrapper">
          <label htmlFor="mobile">Mobile:</label>
          <input
            type="text"
            name="mobile"
            id="mobile"
            value={formState.mobile.value}
          />
        </div>
        <div className="input_wrapper">
          <label className="toc">
            <input
              type="checkbox"
              name="terms"
              checked={formState.terms.value}
            />{" "}
            Accept terms and conditions
          </label>
        </div>
        <div className="input_wrapper">
          <input className="submit_btn" type="submit" value="Sign Up" />
        </div>
      </form>
    </div>
  )
}

export default App
```

In the above code,

- We have introduced a new object `initialState`, which stores the initial state of the form.
- We also have defined a reducer function named `formsReducer`, which does nothing as of now,
  but we will have the logic inside it to update the form state.
- We have introduced `useReducer` hook, which returns the current form state and a dispatch function,
  which will be used to fire form update actions.

If you try to enter any values in the form now,
you will not be able to update it because we don't have any handler functions, which will update our state.

## Adding form handler

Create a folder called `lib` in `src` directory and a file named `formUtils.js` inside it.
This file will have the handler functions which can be reused for other forms.

```js
export const UPDATE_FORM = "UPDATE_FORM"

/**
 * Triggered every time the value of the form changes
 */
export const onInputChange = (name, value, dispatch, formState) => {
  dispatch({
    type: UPDATE_FORM,
    data: {
      name,
      value,
      hasError: false,
      error: "",
      touched: false,
      isFormValid: true,
    },
  })
}
```

Here you could see that we are dispatching the `UPDATE_FORM` action with the value that is being passed to the handler.
As of now, we are setting `hasError` to `false` and `isFormValid` to `true` since we are yet to write the validation logic.

Now in the `App.js` file, update the reducer function to handle the `UPDATE_FORM` action.
Here we are updating the value of the corresponding input field using the `name` as the key.

```js
//...

import { UPDATE_FORM, onInputChange } from "./lib/formUtils"

//...
const formsReducer = (state, action) => {
  switch (action.type) {
    case UPDATE_FORM:
      const { name, value, hasError, error, touched, isFormValid } = action.data
      return {
        ...state,
        // update the state of the particular field,
        // by retaining the state of other fields
        [name]: { ...state[name], value, hasError, error, touched },
        isFormValid,
      }
    default:
      return state
  }
}
```

Now bind the `onInputChange` handler we imported above with the input field for name:

```jsx
<div className="input_wrapper">
  <label htmlFor="name">Name:</label>
  <input
    type="text"
    name="name"
    id="name"
    value={formState.name.value}
    onChange={e => {
      onInputChange("name", e.target.value, dispatch, formState)
    }}
  />
</div>
```

Now you should be able to edit the name field.
Now its time to write the validation logic!

## Adding Validations

Add a function called `validateInput` to `formUtils.js`. Inside this function, we will write validations for all the fields.

```js
export const validateInput = (name, value) => {
  let hasError = false,
    error = ""
  switch (name) {
    case "name":
      if (value.trim() === "") {
        hasError = true
        error = "Name cannot be empty"
      } else if (!/^[a-zA-Z ]+$/.test(value)) {
        hasError = true
        error = "Invalid Name. Avoid Special characters"
      } else {
        hasError = false
        error = ""
      }
      break
    default:
      break
  }
  return { hasError, error }
}
```

Here you can see that in the first if condition, we are checking for empty value since name field is mandatory.
In the second if condition,
we are using RegEx to validate if the name contains any other characters other than the English alphabets and spaces.

> We assume that all the names are in English.
> If you have a user base in regions where the name contains characters outside the English alphabet, you can update the Regex accordingly.

Now update the `onInputChange` function to make use of the validation function:

```js
export const onInputChange = (name, value, dispatch, formState) => {
  const { hasError, error } = validateInput(name, value)
  let isFormValid = true

  for (const key in formState) {
    const item = formState[key]
    // Check if the current field has error
    if (key === name && hasError) {
      isFormValid = false
      break
    } else if (key !== name && item.hasError) {
      // Check if any other field has error
      isFormValid = false
      break
    }
  }

  dispatch({
    type: UPDATE_FORM,
    data: { name, value, hasError, error, touched: false, isFormValid },
  })
}
```

You will also see that we are looping through the `formState` to check
if any of the field is having error to determine the overall validity of the form.

Now let's see if our validation logic works fine. Since we aren't displaying the error message yet, let's log the `formState` and see the values.

### When an invalid name is entered

![Invalid Name Validation](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kcld24spmnqmgerpr6e3.JPG)

> Use `console.table({"name state": formState.name});` for displaying the values of an object in tabular format

### When the name is kept empty

![Empty Name Validation](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/el3gmt50cz4gryegp1bo.JPG)

### When a valid name is entered

![valid Name](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ffwqlvw6bvyb2dbiy28l.JPG)

> You might see that the loggers are getting printed twice each time you type a character:
> this is expected to happen in [Strict Mode](https://reactjs.org/docs/strict-mode.html) in the development environment.
> This is actually to make sure that there are no side effects inside the reducer functions.

## Displaying error message

Before showing the error message, let's add another handler function to our `formUtils.js`

```js
//...
export const onFocusOut = (name, value, dispatch, formState) => {
  const { hasError, error } = validateInput(name, value)
  let isFormValid = true
  for (const key in formState) {
    const item = formState[key]
    if (key === name && hasError) {
      isFormValid = false
      break
    } else if (key !== name && item.hasError) {
      isFormValid = false
      break
    }
  }

  dispatch({
    type: UPDATE_FORM,
    data: { name, value, hasError, error, touched: true, isFormValid },
  })
}
```

You might observe that the `onFocusOut` function is very similar to `onInputChange`,
except that we pass `touched` as `true` in case of `onFocusOut`.
The reason for having additional handler function, which will be bound with the `onBlur` event of the input is
_to show the error messages only when the user finishes typing and moves to the next field_.

Now that we have the error message stored in our state, let's display it:

```jsx
//...
import { UPDATE_FORM, onInputChange, onFocusOut } from "./lib/formUtils"

//...
function App() {
  const [formState, dispatch] = useReducer(formsReducer, initialState)

  return (
    <div className="App">
      <h1 className="title">Sign Up</h1>
      <form>
        <div className="input_wrapper">
          <label htmlFor="name">Name:</label>
          <input
            type="text"
            name="name"
            id="name"
            value={formState.name.value}
            onChange={e => {
              onInputChange("name", e.target.value, dispatch, formState)
            }}
            onBlur={e => {
              onFocusOut("name", e.target.value, dispatch, formState)
            }}
          />
          {formState.name.touched && formState.name.hasError && (
            <div className="error">{formState.name.error}</div>
          )}
        </div>
        {/* ... */}
      </form>
    </div>
  )
}

export default App
```

You will see that we have added `onBlur` handler and we are displaying the error message whenever the form is touched and has errors.

Now let's add some styling for the error message in `App.css`

```css
/*...*/
.error {
  margin-top: 0.25rem;
  color: #f65157;
}
```

Now if you type an invalid name or leave the field empty, you will see the error message:

![Name Validations](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vv9knmwav64t68myztm1.png)

## Adding validation to other fields

Now let's add validation to other fields

Update the `validateInput` function inside `formUtils.js`:

```js
export const validateInput = (name, value) => {
  let hasError = false,
    error = ""
  switch (name) {
    case "name":
      if (value.trim() === "") {
        hasError = true
        error = "Name cannot be empty"
      } else if (!/^[a-zA-Z ]+$/.test(value)) {
        hasError = true
        error = "Invalid Name. Avoid Special characters"
      } else {
        hasError = false
        error = ""
      }
      break
    case "email":
      if (value.trim() === "") {
        hasError = true
        error = "Email cannot be empty"
      } else if (
        !/^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(
          value
        )
      ) {
        hasError = true
        error = "Invalid Email"
      } else {
        hasError = false
        error = ""
      }
      break
    case "password":
      if (value.trim() === "") {
        hasError = true
        error = "Password cannot be empty"
      } else if (value.trim().length < 8) {
        hasError = true
        error = "Password must have at least 8 characters"
      } else {
        hasError = false
        error = ""
      }
      break
    case "mobile":
      if (value.trim() === "") {
        hasError = true
        error = "Mobile cannot be empty"
      } else if (!/^[0-9]{10}$/.test(value)) {
        hasError = true
        error = "Invalid Mobile Number. Use 10 digits only"
      } else {
        hasError = false
        error = ""
      }
      break
    case "terms":
      if (!value) {
        hasError = true
        error = "You must accept terms and conditions"
      } else {
        hasError = false
        error = ""
      }
      break
    default:
      break
  }
  return { hasError, error }
}
```

Note that we have added validation password to have minimum of 8 characters, mobile number to have 10 digits.
Also, you might be wondering about the really long RegEx used for email validation.
You can read more about email validation at [emailregex.com](https://emailregex.com/).

Now let's bind them to the form:

```jsx
//...

function App() {
  const [formState, dispatch] = useReducer(formsReducer, initialState)

  return (
    <div className="App">
      <h1 className="title">Sign Up</h1>
      <form>
        <div className="input_wrapper">
          <label htmlFor="name">Name:</label>
          <input
            type="text"
            name="name"
            id="name"
            value={formState.name.value}
            onChange={e => {
              onInputChange("name", e.target.value, dispatch, formState)
            }}
            onBlur={e => {
              onFocusOut("name", e.target.value, dispatch, formState)
            }}
          />
          {formState.name.touched && formState.name.hasError && (
            <div className="error">{formState.name.error}</div>
          )}
        </div>
        <div className="input_wrapper">
          <label htmlFor="email">Email:</label>
          <input
            type="email"
            name="email"
            id="email"
            value={formState.email.value}
            onChange={e => {
              onInputChange("email", e.target.value, dispatch, formState)
            }}
            onBlur={e => {
              onFocusOut("email", e.target.value, dispatch, formState)
            }}
          />
          {formState.email.touched && formState.email.hasError && (
            <div className="error">{formState.email.error}</div>
          )}
        </div>
        <div className="input_wrapper">
          <label htmlFor="password">Password:</label>
          <input
            type="password"
            name="password"
            id="password"
            value={formState.password.value}
            onChange={e => {
              onInputChange("password", e.target.value, dispatch, formState)
            }}
            onBlur={e => {
              onFocusOut("password", e.target.value, dispatch, formState)
            }}
          />
          {formState.password.touched && formState.password.hasError && (
            <div className="error">{formState.password.error}</div>
          )}
        </div>
        <div className="input_wrapper">
          <label htmlFor="mobile">Mobile:</label>
          <input
            type="text"
            name="mobile"
            id="mobile"
            value={formState.mobile.value}
            onChange={e => {
              onInputChange("mobile", e.target.value, dispatch, formState)
            }}
            onBlur={e => {
              onFocusOut("mobile", e.target.value, dispatch, formState)
            }}
          />
          {formState.mobile.touched && formState.mobile.hasError && (
            <div className="error">{formState.mobile.error}</div>
          )}
        </div>
        <div className="input_wrapper">
          <label className="toc">
            <input
              type="checkbox"
              name="terms"
              checked={formState.terms.value}
              onChange={e => {
                onFocusOut("terms", e.target.checked, dispatch, formState)
              }}
            />
            Accept terms and conditions
          </label>
          {formState.terms.touched && formState.terms.hasError && (
            <div className="error">{formState.terms.error}</div>
          )}
        </div>
        <div className="input_wrapper">
          <input className="submit_btn" type="submit" value="Sign Up" />
        </div>
      </form>
    </div>
  )
}

export default App
```

Now if you test the application, you will see all validations in place:

![All Validations](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tz3asd6s40qs6vqo6crd.png)

Though we have all the validations, we are not validating the form if the user clicks on submit without filling any of the fields.

## Adding form level validation

For the last time, let's add the form level validation

```jsx
import React, { useReducer, useState } from "react"
import "./App.css"
import {
  UPDATE_FORM,
  onInputChange,
  onFocusOut,
  validateInput,
} from "./lib/formUtils"

//...

function App() {
  const [formState, dispatch] = useReducer(formsReducer, initialState)

  const [showError, setShowError] = useState(false)

  const formSubmitHandler = e => {
    e.preventDefault() //prevents the form from submitting

    let isFormValid = true

    for (const name in formState) {
      const item = formState[name]
      const { value } = item
      const { hasError, error } = validateInput(name, value)
      if (hasError) {
        isFormValid = false
      }
      if (name) {
        dispatch({
          type: UPDATE_FORM,
          data: {
            name,
            value,
            hasError,
            error,
            touched: true,
            isFormValid,
          },
        })
      }
    }
    if (!isFormValid) {
      setShowError(true)
    } else {
      //Logic to submit the form to backend
    }

    // Hide the error message after 5 seconds
    setTimeout(() => {
      setShowError(false)
    }, 5000)
  }

  return (
    <div className="App">
      <h1 className="title">Sign Up</h1>
      {showError && !formState.isFormValid && (
        <div className="form_error">Please fill all the fields correctly</div>
      )}
      <form onSubmit={e => formSubmitHandler(e)}>
        <div className="input_wrapper">{/* ... */}</div>
      </form>
    </div>
  )
}

export default App
```

We have added a block error message which will be displayed when the user submits the form and as long as the form is invalid.

Let's add some css to style the error message in `App.css`:

```css
/* ... */

.form_error {
  color: #721c24;
  background-color: #f8d7da;
  border-color: #f5c6cb;
  padding: 0.5rem 1.25rem;
  border: 1px solid transparent;
  border-radius: 0.25rem;
  margin: 1rem 0;
}
```

Now if you click on the submit button without filling the form you should see:

![Block Validation Message](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6vp89g6kwuwxm3qmom8c.png)

# Analyzing the bundle size

Let's see if we were successful in reducing the bundle size by writing our own implementation of form validation.
To do so, first install `webpack-bundle-analyzer` package as a dev dependency:

```bash
yarn add webpack-bundle-analyzer -D
```

Create a file named `analyze.js` in the root directory with the following content:

```js
// script to enable webpack-bundle-analyzer
process.env.NODE_ENV = "production"
const webpack = require("webpack")
const BundleAnalyzerPlugin = require("webpack-bundle-analyzer")
  .BundleAnalyzerPlugin
const webpackConfigProd = require("react-scripts/config/webpack.config")(
  "production"
)

webpackConfigProd.plugins.push(new BundleAnalyzerPlugin())

// actually running compilation and waiting for plugin to start explorer
webpack(webpackConfigProd, (err, stats) => {
  if (err || stats.hasErrors()) {
    console.error(err)
  }
})
```

Run the following command in the terminal:

```bash
node analyze.js
```

Now a browser window will automatically open with the URL [http://127.0.0.1:8888](http://127.0.0.1:8888/)

If you see the bundle size, you will find that our application including form validation utils and css is just 1.67kB gzipped!

![Bundle Size](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9mx8s0882g6x9ngzewkj.png)

# Conclusion

While the form validation libraries have a lot of advantages like
it lets you write less code and if there are a lot of forms in your application, it pays for itself.
But if you are having a simple form and you are concerned about bundle size you can always go for this custom implementation.
Also, if the form is very complex, then again you will have to go for custom implementation
since the form validation libraries might not cover all your use cases.

# Source code and Demo

You can view the complete [source code here](https://github.com/collegewap/react-form-validation) and a [demo here](https://react-form-validation-nine.vercel.app/).