Yup Validation and Formik Form Control in React/VITE

Introduction

In the vast realm of web development, handling form data is a common yet crucial task. With the advent of powerful tools like React and VITE, developers have access to efficient libraries such as Yup and Formik to streamline the form validation process. In this blog post, we'll explore the integration of Yup for validation and Formik for form control in a React/VITE environment.

Main Focus

As previously mentioned, we will go over what exactly makes Yup and Formik a powerhouse in validation and form control. We will explore some standard syntax and choices that come with utilizing both Yup and Formik together. Additionally, we will review some code examples that will break down how to debug/trace when some of your Yup / Formik logic is: !Yup && !Formik.

It is important to note that there are different versions of each of these libraries and it is important to review the library documentation to decide which version best fits your needs. Before we get more into each library, install the latest version of both Yup and Formik (also available with yarn install):

  • npm i yup

  • npm i formik

      import { Formik, Field, Form, ErrorMessage } from 'formik'
      import * as Yup from 'yup'
    

Form Validation with Yup

Yup provides a simple and efficient way to validate form data. It accomplishes this task by leveraging declarative validation methods that are used alongside form fields. Some of the common methods are:

  • .required(): Ensures that the field is not empty.

  • .min() and .max(): Specifies minimum and maximum length or value constraints.

  • .email(): Validates that the input is a valid email address.

  • .url(): Verifies that the input is a valid URL.

  • .matches(): Allows you to define a regular expression pattern for validation.

Custom Validations with.test()

One of the standout features of Yup is the ability to create custom validation rules using the .test() method. This method allows you to define your validation logic based on the field's value and the current form state. Here's a brief example of using .test() to ensure a password meets specific criteria:

const passwordSchema = yup.string()
  .required('Password is required')
  .min(8, 'Password must be at least 8 characters')
  .test('strong-password', 'Password must contain at least one uppercase letter, one lowercase letter, and one number', value => {
    return /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).+$/.test(value);
  });

In the code snippet below you can see the validationSchema established using Yup. This schema ensures that various fields like artist, albumCover, and others adhere to specific validation rules. For instance, the artist field must be a non-empty string, and similar rules apply to other fields.

//Validation using Yup
//Create a validationSchema for each form input and customize the error message

const validationSchema = Yup.object().shape({
    inCollection: Yup.boolean().required('Collection status is required'),
    artist: Yup.string().required('Artist is required'),
    albumCover: Yup.string().required('Album cover image link is required'),
    title: Yup.string().required('Title is required'),
    released: Yup.string().required('Release year is required'),
    label: Yup.string().required('Label is required'),
})

Form Control with Formik

Formik simplifies handling form state, form submission, and validation. It accomplishes this through built-in utilities and in the coming code examples we will utilize a few of these utilities. One thing to mention is you will notice the lack of useState. This is because one benefit of Formik is that it automatically manages form state, eliminating the need for developers to manually handle state changes and updates. Additionally, it also supports asynchronous form submission.

In the examples provided, we are using Formik Component Integration within our AlbumsForm component. This integration allows for seamless integration into React applications as it allows developers to wrap their components with the Formik high-order component.

Breaking Down The Code:

We initialize Formik with an initialValues object, defined validationSchema by Yup, and an onSubmit function to handle form submissions. The combination of Yup and Formik is what makes these libraries very resourceful. There are a few syntax variations but below I have highlighted what is the preferred method based on the library documentation.

    const initialValues = { //Establish your forms initial values
        inCollection: true,
        artist: '',
        albumCover: '',
        title: '',
        released: '',
        label: '',
    }
    // I had to comment out the below return for text clarity
    // Remove the below comment (//) and keep the return (
    //return ( 
        <div>
        {formStatus && <div style={{ color: 'green' }}>{formStatus}</div>}
        <Formik
            initialValues={initialValues}
            validationSchema={validationSchema} //validates using validationSchema
            onSubmit={async (values, { //pass desired formik functions to assist with form control
                setSubmitting, resetForm, setStatus }) => {
            try {
                await handleAddAlbum(values) // Callback to handle POST
                setFormStatus('Form submitted successfully!') // Message appears on successful POST
                setTimeout(() => {
                    navigate('/') // Navigate back to the main library after ...
                    setFormStatus('') // reset the displayed Formstatus back
                }, 2000) // 2000 milliseconds / 2 seconds
                resetForm()
            } catch (validationError) { //upon Submit > forEach field
                const errors = {} // not completed display a error at the top of the form
                validationError.inner.forEach((e) => {
                errors[e.path] = e.message
                })
                setStatus({}) //removes prior status if one was set
                setSubmitting(false) //setSubmitting handles form control 
            }
            }}
        >

In the above code example, we used quite a few different Formik-specific utilities such as functions that directly assist with form control.

  • setSubmitting is particularly useful when dealing with asynchronous form submissions. After initiating an asynchronous operation (e.g., submitting data to a server), you can call setSubmitting(true) to indicate that the form is in the process of being submitted. Once the operation is complete, you can use setSubmitting(false) to mark the end of the submission process.

  • After successfully submitting a form or when a "reset" action is triggered, you can call resetForm() to reset the form.

  • You can use setStatus to update a status message or store additional data related to the form. For example, you might use it to display a success message or handle custom scenarios specific to your application's requirements.

validationError is a property and not a function as when a validation error occurs during form submission, Formik sets the validationError property to the validation error message. This can be useful for displaying a general error message related to the entire form, not tied to a specific field.

The form renders input fields for attributes like artist name, album cover link, and more. Any validation errors are displayed user-friendly below the corresponding input fields or another spot if you desire. Within the example code below, I highlight the two options I considered.

Display Errors / Successful Form submission at the top of the form:

//.. return ( and prior code above
        <div>
        {formStatus && <div style={{ color: 'green' }}>{formStatus}</div>}
        <Formik
            initialValues={initialValues}
            validationSchema={validationSchema} //validates using validationSchema

//.. addtional JSX here

            {({ isSubmitting, submitCount, errors }) => (
            <Form>
       {/* Displaying each error mapped at the top of the form */}
                {submitCount > 0 &&
                Object.keys(errors).map((field) => (
                    <div key={field} style={errorStyle}>
                    {errors[field]}
                    </div> 
                ))}

Display Errors beneath each form option:


//.. prior JSX here
        >
            {({ isSubmitting }) => (
            <Form>
            {fieldInfo.map((field) => (
                <div key={field.name}>
                <label htmlFor={field.name}>{field.label}:</label>
                {field.type === 'checkbox' ? (
                    <Field type={field.type} id={field.name} name={field.name} />
                ) : (
                    <Field
                    type={field.type}
                    id={field.name}
                    name={field.name}
                    placeholder={field.placeholder}
                    />
                )}
// Remove these two lines for code to work
// Below the inputs is where the ErrorMessages will render 
                <ErrorMessage name={field.name} component="div" style={errorStyle} />
                </div>
            ))}
            <button type="submit" disabled={isSubmitting}>
                Submit
            </button>
            </Form>
        )}
        </Formik>
    </div>
    )
    }

//.. addtional JSX here , repeat similar syntax for each form input

In the above code, we utilized the Formik components named Field and ErrorMessage.

  • The Field component simplifies the binding of form elements to the underlying form state. This is exactly what we previously mentioned when we said Formik would handle the form state for us and allow us to easily manage specific field values.

  • The ErrorMessage displays error messages that are associated with a specific form field that is leveraged through field-level validation. The name prop of <ErrorMessage> should match the name prop of the corresponding <Field> component. The error message defined in the validate function will be displayed within the <div> element and the component prop specifies the type of HTML element or custom component that should be used to render the error message.

Tip: You can even customize these errors with a variable, CSS, or even a CSS library.

const errorStyle = { color: 'red' , fontWeight: 'bold'}

Final note: When the user leaves the input field, the onBlur event handler will be called. This will update the touched object in the Formik state, which will indicate that the field has been visited. Formik Handles all of this for you! Be careful if you choose to utilize the useFormik (hook) instead, as it has less functionality.

Debugging Your Validation/Form Control

Nothing is more difficult than learning the intricacies of a new library and fumbling through Syntax and errors.

Debugging in general can be a full topic of its own with many references and developer tool examples. With that said, I wanted to slightly touch on it by getting you started with something as an example.

I am showing mostly console.log() debugging below and with only one debugger. I would suggest getting familiar with a debugger as it can be much more useful in following the code stack.


     <div>
      {formStatus && <div style={{ color: 'green' }}>{formStatus}</div>}
      <Formik
          initialValues={initialValues}
          validationSchema={validationSchema} //validates using validationSchema
          onSubmit={async (values, { //pass desired formik functions to assist with form control
              setSubmitting, resetForm, setStatus }) => {
          try {
              debugger 
              console.log("Before handleAddAlbum")
              await handleAddAlbum(values) // Callback to handle POST
              console.log("After handleAddAlbum")
              console.log("After handleAddAlbum")  
              console.log("Before setStatus:", Formik)
              setFormStatus('Form submitted successfully!') // Message appears on successful POST
              console.log("after setStatus:", Formik)
              setTimeout(() => {
                  navigate('/') // Navigate back to the main library after ...
                  setFormStatus('') // reset the displayed Formstatus back
              }, 2000) // 2000 milliseconds / 2 seconds
              resetForm()
              console.log("After resetForm:", Formik)
          } catch (validationError) { //upon Submit > forEach field
              const errors = {} // not completed display a error at the top of the form
              validationError.inner.forEach((e) => {
              errors[e.path] = e.message
              })
              console.log("After setErrors:", Formik)
              setStatus({}) //removes prior status if one was set
              setSubmitting(false) //setSubmitting handles form control 
          }
          }}

Conclusion

Form validation and control can often be a daunting task for new developers and a tedious one for more senior developers. Through utilizing a form validation and form control library such as the Yup/Formik combination, you will learn to leverage these libraries so you can create a robust and user-friendly form.

Nevertheless, no good library is understood without first going through that library's supportive material and I highly suggest reviewing the related information and reference links before getting started on your own Yup/Formik controlled form.

MDN - Native/Basic Form Controls

MDN - Form Validation

MDN - Statements: try...catch

NPM - Node package manager

Additional Resources Provided Directly by Formik

References:

Yup - Documentation

jquense/yup: Dead simple Object schema validation (Very detailed)

Formik - Library Overview / Documentation

Formik - useHook vs <Formik

Formik - <Form />

Formik - <ErrorMessage />

Formik - <Field />

Formik - How Validation Processes