How to use Formik

I want to thank codevolution for his amazing work in general, and more specifically for his playlist on Formik. If by any luck you found parts this post interesting, I strongly advice you to watch the playlist, it is much more valuable. Indeed the goal of this post is more about getting quickly the useful information about Formik and it is not focused on any pedagogic approach.

Introduction

Formik is a small library that helps you deal with forms in React:

  • Managing form data

  • Form submission

  • Form validation and displaying error messages

A simple Form

The markup

The form and input element are styled using styled-components:

import styled from 'styled-components';
export const Form = styled.form``;
export const Input = styled.input`
  display: block;
  width: 400px;
  padding: 6px 12px;
  font-size: 14px;
  line-height: 1.42857143;
  color: #555;
  background-color: #fff;
  background-image: none;
  border: 1px solid #ccc;
  border-radius: 4px;`
;

The form component itself is a fonctionnal component:

import React from 'react';
import { Form, Input } from './Form';

const SimpleForm = () => {  
return (    
<div>
      <Form action=''>
        <label htmlFor='name'>Name</label>
        <Input type='text' name='name' id='name' />
        <label htmlFor='email'>Email</label>
        <Input type='email' name='email' id='email' />
        <label htmlFor='channel'>Channel</label>
        <Input type='text' name='channel' id='channel' />
        <button>Submit</button>
      </Form>
    </div>
  );
};
export default SimpleForm;

useFormik hook and the state of the form

The goal is to use formik to turn the three input fields into controlled components:

Import formik:

import { useFormik } from 'formik';

Call the useFormik hook and initialize its initialValues properties:

const formik = useFormik({
    initialValues: {
      name: '', //name of the attribute
      email: '', //name of the attribute
      channel: '', //name of the attribute
    },
});

Finally, use formik to set the onChange and the value properties of each input fields:

<Input
          type='text'
          name='name'
          id='name'
          onChange={formik.handleChange}
          value={formik.values.name}
/>

<Input
          type='email'
          name='email'
          id='email'
          onChange={formik.handleChange}
          value={formik.values.email}
/>

<Input
          type='text'
          name='channel'
          id='channel'
          onChange={formik.handleChange}
          value={formik.values.channel}
/>

Handling form submission

To handle submission, first on the form element add the onSubmit handler:

<Form action='' onSubmit={formik.handleSubmit}>

And finally implements the submission logic in the formik configuration object:

  const formik = useFormik({
    initialValues: {
      name: '', //name of the attribute
      email: '', //name of the attribute
      channel: '', //name of the attribute
    },
    onSubmit: (values) => { //values contain the state of the form
      console.log(values);
    },
  });

Handling form validation

First add a validate property to the formik configuration object:

validate: (values) => {
      //values: contain values.name, values.email, values.channel
      //1. Must return an object
      //2. Object keys must match keys of values object
      //3. Values must be String
      let errors = {};
      if (!values.name) {
        errors.name = 'Required';
      }
      if (!values.email) {
        errors.email = 'Required';
      }
      if (!values.channel) {
        errors.channel = 'Required';
      }
      return errors; //validate must return an object
    },

Second, you need to process the formik.errors object to display validation error when it is appropriate:

{formik.errors.name ? <div>{formik.errors.name}</div> : null}

Trigger validation only after field has been visited

First on the input element add the onBlur handler:

onBlur={formik.handleBlur}

The information processed through this handler is available in formik.touched

To display the validation error message only if the field has already been visited, use the following logic:

{formik.touched.name && formik.errors.name ? (
            <Error>{formik.errors.name}</Error>
) : null}

Shema validation with Yup

Install and import yup:

yarn add yup
import * as Yup from 'yup'

Create a validation object schema which contains the rules for the form fields:

const validationSchema = Yup.object({
  name: Yup.string().required('Required'),
  email: Yup.string().email('Invalid email format').required('Required'),
  channel: Yup.string().required('Required')
});

In formik configuration object, instead of declaring the validate property, you must use the validationSchema property:

const YoutubeForm = () => {  const formik = useFormik({
    initialValues,
    onSubmit,
    validationSchema,
})

Reducing boilerplate

On every input elements, many lines of code are the same or at least very similar (the only difference can be the input name). To avoid these dupplicated lines of code, formik provides the getFieldProps helper method which need to be called on each input element. Use:

{...formik.getFieldProps('name')}

Instead of:


onChange={formik.handleChange}
onBlur={formik.handleBlur}
value={formik.values.name}

Formik components

The formik component is a replacement for the useFormik hook. Import the Formik components and remove the call to the useFormik hook:

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

Wrap the entire form with the Formik component, replace the form html element with the Form formik component, replace the input html element with the Field component. Replace the rendering error message block by the ErrorMessage component

<Formik
  initialValues={initialValues}
  onSubmit={onSubmit}
  validationSchema={validationSchema}>
  <Form>
    //the form
    <Field type='text' name='name' id='name' />
    <ErrorMessage name='name'/>
    //the form
    <Field type='email' name='email' id='email' />
    <ErrorMessage name='email'/>
    //the form
    <Field type='text' name='channel' id='channel' />
    <ErrorMessage name='channel'/>
  </Form>
</Formik>

More about Field component

By default it does two things:

  • by default it renders an html input element

  • it hooks up the input element to formik (handleChangle, handleBlur and value)

To create a text area:

<Field as='textarea' name='comments' id='comments' />

The as property on the field component can take:

  • input

  • select

  • textarea

  • A valid HTML element name

  • A custom React component

Field can also be rendered using the render props pattern:


<Field name='address'>
  {(props) => {
    const { field, form, meta } = props;
    return (
      <div>
        <input type='text' id='address' {...field} />
        {meta.touched && meta.error ? <div>{meta.error}</div> : null}
      </div>
    );
 }}
</Field>

More about the ErrorMessage component

It takes name props and display an error message if the field with that name has been visited and an error message exists for that field.

You can use the additionnal component props to wrap the error message.

Ex:

<ErrorMessage name='name' component="div"/>

The error message is now wrapped in a div.

The components props can also take a react components:

Ex:

import React from 'react';
import styled from 'styled-components';

const ChannelErrorWrapper = styled.div``;

export const ChannelError = (props) => {
  return <ChannelErrorWrapper>{props.children}</ChannelErrorWrapper>;
};

And :

<ErrorMessage name='channel' component={ChannelError} />

It is also possible to use the render props pattern:

<ErrorMessage name='email'>
            {
              (errorMsg) => {
                return <div>{errorMsg}</div>
              }
            }
</ErrorMessage>

Nested objects and Arrays

You make want organize the input values of some components together. You can organize them as a nested objects or as an arrays.

Let's says an adress is composed of inputs line1, line2, postalCode and city. Group these by nested object means something like that in the default value: address: {line1:"", line2:"", postalCode:"", city:""}. On the other hand to organize the data as an array means to have something like that in the default value address:['','','',''].

Next in the input fields themselves, the way you reference the value through the name attribute of the input element is a little different. If you are using nested object you will use name="adress.line1", name="adress.line2", name="adress.postalCode" and name="adress.city". On the other hand if you are grouping your data using array, you will use name="adress[0]", name="adress[1]", name="adress[2]" and name="adress[3]"

FieldArray component

In some forms, user has the ability to add/remove dynamically an input field. The purpose of the FieldArray component is to propose a way to achieve this goal.

First import the FieldArray component:

import { FieldArray } from 'formik';

In the default value, declare a new property whose value is an array. In the example the user will add/remove input element to add new line for his adress:

const initialValues = {
  adressLines: ['']
};

In the form itself:

<div className='form-control'>
          <label>Adress lines</label>
          <FieldArray name='adressLines'>
            {(fieldArrayProps) => {
              const { push, remove, form } = fieldArrayProps;
              const { values } = form;
              const { adressLines } = values;
              return (
                <div>
                  {adressLines.map((adressLine, index) => (
                    <div key={index}>
                      <Field name={`adressLines[${index}]`} />
                      {index > 0 && (
                        <button type='button' onClick={() => remove(index)}>
                          -
                        </button>
                      )}
                    </div>
                  ))}
                  <button type='button' onClick={() => push('')}>
                    +
                  </button>
                </div>
              );
            }}
          </FieldArray>
</div>

When does validation run

  • A change event has occur

  • A blur event has occur

  • Form submission is attempted

There are props on the Formik component itself to control the two first cases:

    <Formik
      initialValues={initialValues}
      validationSchema={validationSchema}
      onSubmit={onSubmit}
      enableReinitialize
      // validateOnChange={false}
      // validateOnBlur={false}>

Field Level validation

At the form level you have two options:

  • validate funtion

  • validationShema if using Yup validation

    <Formik
      ...
      validationSchema={validationSchema}
      //validate= {validate}
     ...>

It is also possible to define validation at the Field level.

First create a validate funtion :

const validateComments = value => {
  let error
  if (!value) {
    error = 'Required'
  }
  return error
}

Next assign the newly created validation function to the Field itself through the validate property:

<Field
      as='textarea'
      id='comments'
      name='comments'
      validate={validateComments}
/>

Manually trigering validation

To trigger manual validation you must use the render props pattern on the top Formik components. It will give you access the formik props. :

 <Formik
      initialValues={formValues || initialValues}
      validationSchema={validationSchema}
      onSubmit={onSubmit}>
      {
formik
 => {
        console.log('Formik props', formik)
        return (
          <Form>....</Form>
        )}
      }
</Formik>

Using the formik object you can control both field and form validation.

To trigger field validation:

<button
   type='button'
   onClick={() => formik.validateField('comments')}>
   Validate comments
</button>       

To display error message, the error message component wait until it has been touched. To emulate this:

 <button
   type='button'
   onClick={() => formik.setFieldTouched('comments')}              >Visit comments</button>

To trigger form validation use

formik.validateForm()
and to enable the validation error message to be displayed:

formik.setTouched({
                  name: true,
                  //set all the name of the components
})

Disabling submit

You must use the render props pattern on the top Formik components to get access to the formik props.

To disable the submit button if the form is not valid:

<button
              type='submit'
              disabled={!formik.isValid}
>
              Submit
</button>

On page load, since the validation rules are not run the submit button will not be disabled. If this is an issue, there are two possible solutions:

  • run the validation on page load: use the validateOnMount prop on the top Formik component.

  • update disabilty rule of the submit button, make use of the dirty flag. This will only be useful if the form is not submittable as is:

<button
              type='submit'
              disabled={!(formik.dirty && formik.isValid)}
>
              Submit
</button>

To disable the submit button while the form submission is being processed:

<button
              type='submit'
              disabled={formik.isSubmitting}
>
              Submit
</button>

When the form is submitted formik will set the isSubmitting flag to true, but it is your responsability to set it back to false.

const onSubmit = (values, submitProps) => {
  submitProps.setSubmitting(false)
  //enableReinitialize props on the top Formik components
  //so that you can reset the form
  submitProps.resetForm() 
}

Load saved data

You can use a state variable to hold the form values:

import React, { useState } from 'react'
const [formValues, setFormValues] = useState(null)
<Formik
      initialValues={formValues || initialValues}
      enableReinitialize
      ...
/>

Then to load saved data you first need to fetch them and then set the state variable:

setFormValues(savedValues)

Reset Form Data

You can do that from a button:

<button type='reset'>Reset</button>

You can reset the form data from the submit handler:

const onSubmit = (values, submitProps) => {
   ...
   submitProps.resetForm()
}