DEV Community

Cover image for No more tears, handling Forms in React using Formik, part II
Chris Noring for Microsoft Azure

Posted on

No more tears, handling Forms in React using Formik, part II

Follow me on Twitter, happy to take your suggestions on topics or improvements /Chris

This is the continuation of our first part on Formik, the amazing Forms library for React

This article is part of a series:

In this article we will cover:

  • Schema Validation with Yup, there is an alternate way to validate your input elements and that is by declaring a schema in Yup and simply assign that to an attribute on the Formik component
  • Async validation
  • Built-in components, make everything less verbose using some of Formiks built-in components

 Resources

I have made a repo for both these articles, so if you get stuck have a look here Form demo repo

Built-in components

So far we have been using regular HTML elements like form and input to build our form and we have connected to events like onSubmit, onChange and onBlur. But we can actually be typing a lot less. Say hello to the following components:

  • Form, this replaces a normal form element
  • Field, this replaces any type of input element
  • ErrorMessage, this doesn't really replace any controls that you have but is a great component that given the attribute name is able to show your error message

Let's first look at a simple form and then rewrite it using the above-mentioned components:

import { Formik } from 'formik';
import React from 'react';

const FormikExample = () => (
  <Formik
    initialValues={{ name: '' }}
    validation={values => {
      let errors = {};
      if(!values.name) {
        errors.name = 'Name is required';
      }
      return errors;
    }}
    onSubmit={values ={
      console.log('submitted');
    }}
  >
  {({ handleSubmit, handleChange, values, errors }) => (
   <form onSubmit={handleSubmit}>
    <input name="name" onChange={handleChange} value={values.name} />
    {errors.name && 
    <span>{errors.name}</span>
    }
   </form>
  )
  }
  </Formik>
)
Enter fullscreen mode Exit fullscreen mode

Ok, above we see what a minimal implementation looks like the classical way of doing it, that is using HTML elements like form and input.

Now lets clean this up using Formiks built-in controls:

import { Formik, Form, Field, ErrorMessage } from 'formik';
import React from 'react';


const FormikExample = () => (
  <Formik
    initialValues={{ name: '' }}
    validation={values => {
      let errors = {};
      if(!values.name) {
        errors.name = 'Name is required';
      }
      return errors;
    }}
    onSubmit={values ={
      console.log('submitted');
    }}
  >
  {({ handleSubmit, errors }) => (
   <Form onSubmit={handleSubmit}>
    <Field type="text" name="name" />
    <ErrorMessage name="name"/>
    }
   </Form>
  )
  }
  </Formik>
)
Enter fullscreen mode Exit fullscreen mode

Not super impressed? Let's list what we don't need to type anymore:

  • the onChange disappears from each input element
  • the input element is replaced by Field component
  • the form element is replaced by Form component
  • the conditional {errors.name && disappears as well as ErrorMessage component takes care of that bit

Not enough? Well imagine you have 10 fields, that is at least 10 lines of code that disappears and it generally it just looks cleaner. Now to our next improvement, we can replace our validation() function with a schema, up next.

Schema validation with Yup

Ok, we've covered how we can really clean up our markup by using the builtin controls Form, Field and ErrorMessage. Next step is improving even more by replacing our validation property with a validationSchema property. For that to be possible we need to define a schema using the library Yup. So what does a schema look like :

import * as Yup from 'yup'

const schema = Yup.object().shape({
    firstName: Yup.string()
      .min(2, 'Too Short!')
      .max(50, 'Too Long!')
      .required('Required'),
    lastName: Yup.string()
      .min(2, 'Too Short!')
      .max(50, 'Too Long!')
      .required('Required'),
    email: Yup.string()
      .email('Invalid email')
      .required('Required'),
  });
Enter fullscreen mode Exit fullscreen mode

The above schema defines three different fields firstName, lastName and email and gives them each attributes that they should adhere to:

  • firstName, this should be a string consisting of min 2 characters and maximum 50 characters and its also required
  • lastName, this is also a string with the same min/max requirements and it's also required
  • email, this is just a string that is required

As you can see the above is quite readable and by defining your data like this you save yourself from having to type a lot of if constructs checking if all attributes are fulfilled.

Let's now put it to use in our Formik element, like so:

<Formik validationSchema={schema}>
Enter fullscreen mode Exit fullscreen mode

That's it, that is all you need to define your form data in a really expressive way, doesn't that give you a warm and fuzzy feeling? :)

Async validation

Ok, now to our last topic, asynchronous validation. So what's the scenario? Well, sometimes you have data that you can't really tell on client side only whether the entered value is correct or not. Imagine you have a form where you want to find out whether a company or certain web page domain is already taken? At that point, you most likely will need to make a call to an endpoint and the endpoint will not be coming back with the answer instantly.

Ok, we've set the scene, how do we solve this in Formik? Well, the validation property is able to accept a Promise as well. Really, you think? That easy? Well, the solution is in my mind a bit unorthodox, let me show you what I mean:

<Formik
  validate={values => {
    console.log('validating async');
    let errors = {};
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        errors.companyName = 'not cool';
        resolve('done');
      },3000);
      }).then(() => {
        if(Object.keys(errors).length) {
          throw errors;
        }
      });
    }}
>
// define the rest here
</Formik>
Enter fullscreen mode Exit fullscreen mode

Looking at our validate implementation we see that we create a Promise that internally runs a setTimout to simulate it going to an endpoint that it takes time to get an answer from. At this point we set a errors.companyName to an error text:

setTimeout(() => {
  errors.companyName = 'not cool';
  resolve('done');
},3000);
Enter fullscreen mode Exit fullscreen mode

In more real scenario we would probably call a function and depending on the functions answer we would possibly assign errors.companyName. I'll show you below what I mean:

isCompanyNameUnique(values.companyName).then(isUnique => {
  if(!isUnique) {
    errors.companyName = `companyName is not unique, please select another one`
  }
  resolve('done')
})
Enter fullscreen mode Exit fullscreen mode

Next thing that happens in our code is that we invoke then(), that happens when we call resolve(). Something really interesting happens in there, we check the errors for any properties that might have been set and if so we throw an error with our errors object as an argument, like so:

.then(() => {
  if(Object.keys(errors).length) {
    throw errors;
  }
});
Enter fullscreen mode Exit fullscreen mode

I don't know about you, but to me, this looks a bit weird. I would have thought providing validation with a Promise would have meant that a reject() of the Promise would have been a more intuitive way of doing it, like so:

// this to me would have been more intuitive, but observe, this is NOT how it works, so DONT copy this text but refer to the above code instead

validation={ values => 
  console.log('validating async');
  let errors = {};
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      errors.companyName = 'not cool';
      reject(errors);
    },3000);
  })
}}
Enter fullscreen mode Exit fullscreen mode

Async on field level

So far we have shown how to do async validation on Forms level but if you think about would you really want that? Most likely you have a mix of fields where it's enough to validate some of them client side while only a minority if fields need async validation. In such a case it makes sense to apply validation per field. That is quite easy to achieve by typing like this:

<Field name="username" validate={this.validate} >
Enter fullscreen mode Exit fullscreen mode

This is probably preferred if you got async validation on a field. As for the other fields, you can validate client side it's probably a good idea to define those in on the Formik components validationSchema and use Yup schemas for that as we've described above.

Words of caution

If we do have async validation in there make sure your validations don't run too often especially if the validation takes time. You don't want a 3-sec validation to trigger every time a key is typed, at most you want it when the user leaves the field to start typing in another field, we refer to this as the blur event. So make sure you set up your Formik component like this:

<Formik
  validateOnBlur={true} 
  validateOnChange={false} >
Enter fullscreen mode Exit fullscreen mode

This does what you want, setting validateOnBlur to true is what you want, even though technically this is true by default. You want to be explicit with the next one though validateOnChange. You want this to be off, or set to false.

Summary

We've set out to cover built-in components like Form, Field and ErrorMessage, the end result was us cleaning up a lot of code.

Furthermore, we showed how we could get rid of our validation function by defining a schema using the Yup library.

Finally, we covered asynchronous validation and we discussed things to consider like when to validate and that it is probably best to have a field level validation for those few asynchronous fields that we have in a form and to use schema validation for the remaining fields.

That's it, that was the end of our article. I hope this part and the previous one have given you new hope that dealing with Forms in React doesn't have to that painful

Top comments (10)

Collapse
 
marcelgeo profile image
MarcelGeo

Is it possible to use validationSchema and async validation simultaneously for one Formik Component? Thanks for usefull tutorial. Yup looks mutch smarter than AvForm in Bootstrap.

Collapse
 
softchris profile image
Chris Noring • Edited

I believe so.. Let me see if I can come with an example of that to show how..

Yea here I have such an example github.com/softchris/formik-exampl...

Collapse
 
_ali_ profile image
ali • Edited

Hi Chris,
I tried using Formik,Yup and MaterialUi together. But it seems like there is a considerable lag when typing in the textfields (of the materialUI) present in the form. The form that I'm using contains many inputs, maybe why the lag is being caused. Any way to avoid this delay ?. Also is there a full rendering of the component, when the validation is done ?

Collapse
 
emielvangoor profile image
Emiel van Goor

My best guess is that the validation is triggered on every keystroke. You should try to disable it with

validateOnBlur={false}
validateOnChange={false}

Collapse
 
mvarsakelis profile image
mvarsakelis

Thanks Chris. This was a really good article. I have implemented a form with Formik and got all the validation hooked up. For the last week I have been struggling on how to show an alert on success or failure after the submission. In my case I call an API to add a record to a database. If a record was successfully added the API response will have stuff and I display a success alert. If it fails the response is empty and I display an error alert. What I want to do is retain the form field values so if there was an error the form is not blank when I close the alert.

I feel like this is something to do with the state of the form and/or fields and that I need to do something in the on submit. Like ...values or ...prevState or something.

This is currently my onSubmit area of the form:

        onSubmit={async (
          values,
          { resetForm, setSubmitting, isSubmitting }
        ) => {
          apiService.addCompanyMaster(values).then((response) => {
            setApiResponse(response)
            setShowAlert(true)
          })
        }}
Enter fullscreen mode Exit fullscreen mode

Thanks in advance if you have any hints

Collapse
 
ambroseus profile image
Eugene Samonenko

it was very interesting challenge to combine Formik, yup, material-ui and react-select 2... and it works all together! :)

Collapse
 
ambroseus profile image
Eugene Samonenko

one feature was very helpful in case of using redux with data for initialValues: enableReinitialize

Collapse
 
psycho5 profile image
Sagar Baver

Hey Chris!

I believe you meant to use <Form> built-in component in your first code snippet instead of <form>.

Collapse
 
softchris profile image
Chris Noring

aah... thank you :)

Collapse
 
slick profile image
Majid Shah

This article is amazing , you have explianed it very well. Please make part 3 touching useFormikContext() and handling parts of form in child components