DEV Community

Ioannis Potouridis
Ioannis Potouridis

Posted on

 

Form validation in React, as simple as it gets

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.

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

And our form component looks like this.

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.

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.

<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.

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.

<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.

Top comments (0)

Top Posts from the React Ecosystem

1. Changes In The Official React Documentation

The former React Docs Beta has been officially released as the updated React documentation at react.dev after years of hard work and refinement. Check out the brand new React Docs: What’s New in the Updated React Docs

2. CRA's Time is Over

React developer team has removed create-react-app (CRA) from official documentation rendering it no longer the default setup method for new projects. The bulky setup, slow, and outdated nature of CRA led to its removal: create-react-app is officially dead

3. How to Fetch Dev.to Articles for Your Portfolio

Integrate the articles of your Dev.to profile into your personal portfolio with either React, Vue, or Next.js by following these simple steps. It outlines how to include frontend to pull the information and correctly utilizes the Dev.to API: How to Fetch Your Dev.to Articles for Your Portfolio with React