Form validation in React, as simple as it gets
Tags: react, tutorial, typescript
Published: 2020-04-14 07:11:30

There are a lot of form or object schema validation libraries, such as `react-hook-form`, `formik`, `yup` to name a few. In this example we are **not** going to use any of them.

To start with, we are going to need a state to keep our values. Let's say the following interface describes our values' state.

```jsx
interface Values {
  firstName: string;
  password: string;
  passwordConfirm: string;
}
```

And our form component looks like this.

```jsx
const initialValues: Values = {
  firstName: '',
  password: '',
  passwordConfirm: '',
}

function Form() {
  const [values, setValues] = useState<Values>(initialValues);

  const handleChange = ({ target }: React.ChangeEvent<HTMLInputElement>) => {
    setValues((prev) => ({ ...prev, [target.name]: target.value }));
  };

  return (
    <form>
      <label htmlFor="firstName">First name</label>
      <input
        id="firstName"
        name="firstName"
        onChange={handleChange}
        type="text"
        value={values.firstName}
      />

      <label htmlFor="password">Password</label>
      <input
        id="password"
        name="password"
        onChange={handleChange}
        type="password"
        value={values.password}
      />

      <label htmlFor="passwordConfirm">Confirm password</label>
      <input
        id="passwordConfirm"
        name="passwordConfirm"
        onChange={handleChange}
        type="password"
        value={values.passwordConfirm}
      />
    </form>
  )
}
```

All we need is an errors object that is calculated based on our current values' state.

```jsx
const errors = useMemo(() => {
  const draft: { [P in keyof Values]?: string } = {};
  
  if (!values.firstName) {
    draft.firstName = 'firstName is required';
  }

  if (!values.password) {
    draft.password = 'password is required';
  }

  if (!values.passwordConfirm) {
    draft.passwordConfirm = 'passwordConfirm is required';
  }

  if (values.password) {
    if (values.password.length < 8) {
      draft.password = 'password must be at least 8 characters';
    }

    if (values.passwordConfirm !== values.password) {
      draft.passwordConfirm = 'passwordConfirm must match password';
    }
  }
    
  return draft;
}, [values]);
```

Then, you'd modify your JSX in order to display the error messages like so.

```jsx
<label htmlFor="firstName">First name</label>
<input
  aria-describedby={
    errors.firstName ? 'firstName-error-message' : undefined
  }
  aria-invalid={!!errors.firstName}
  id="firstName"
  name="firstName"
  onChange={handleChange}
  type="text"
  value={values.firstName}
/>
{errors.firstName && (
  <span id="firstName-error-message">{errors.firstName}</span>
)}
```

Now the messages appear when we first see the form but that's not the best use experience that we can provide. In order to avoid that there are two ways:

1. Display each error after a user interacted with an input
2. Display the errors after user submitted the form

With the first approach we would need a `touched` state, where we keep the fields that the user touched or to put it otherwise, when a field loses its focus.

```jsx
const [touched, setTouched] = useState<{ [P in keyof Values]?: true }>({});

const handleBlur = ({ target }: React.FocusEvent<HTMLInputElement>) => {
  setTouched((prev) => ({ ...prev, [target.name]: true }));
};
```

And our field would look like this.

```jsx
<label htmlFor="firstName">First name</label>
<input
  aria-describedby={
    touched.firstName && errors.firstName
      ? 'firstName-error-message'
      : undefined
  }
  aria-invalid={!!touched.firstName && !!errors.firstName}
  id="firstName"
  name="firstName"
  onBlur={handleBlur}
  onChange={handleChange}
  type="text"
  value={values.firstName}
/>
{touched.firstName && errors.firstName && (
  <span id="firstName-error-message">{errors.firstName}</span>
)}
```

In a similar way, we would keep a `submitted` state and set it to `true` when a user submitted the form for the first time and update our conditions accordingly.

And, that's it!

It may be missing a thing or two, and may require you writing the handlers and the `if` statements for calculating the errors, but it's solid solution and a good start to validate forms in React.