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()
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()
}