DEV Community

Ioannis Potouridis
Ioannis Potouridis

Posted on

4

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.

Image of Timescale

🚀 pgai Vectorizer: SQLAlchemy and LiteLLM Make Vector Search Simple

We built pgai Vectorizer to simplify embedding management for AI applications—without needing a separate database or complex infrastructure. Since launch, developers have created over 3,000 vectorizers on Timescale Cloud, with many more self-hosted.

Read more

Top comments (0)

Image of Docusign

🛠️ Bring your solution into Docusign. Reach over 1.6M customers.

Docusign is now extensible. Overcome challenges with disconnected products and inaccessible data by bringing your solutions into Docusign and publishing to 1.6M customers in the App Center.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay