Writing a React form without any additional dependencies isn’t much of a challenge itself. The same goes for applying client-side data validation. What might be troublesome is keeping all your forms consistent across a bigger project. If every developer in your team has a slightly different approach, it may turn out to be messy. With Formik, we can keep all of our forms in check and apply a foreseeable approach to data validation. In this article, we aim to learn its basics and advantages. We also get to know the Yup library and how we can use it to write a validation schema.
The advantages of using Formik
The thing that we want to avoid is letting our library do too much. So much even, that we don’t get what happens underneath. Thankfully, with Formik, everything is straightforward. It helps us get values in and out of a form state, validate it, and handle the form submission. The above things are not difficult to do without the help of Formik, but it helps us organize things better. It makes testing and refactoring way easier.
You might think of using Redux for that, but keeping every piece of the state of your form in it is not necessarily a good idea. Let’s start by creating a form without Formik:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 |
import React, { Component } from 'react'; import isFormValid from './isFormValid'; import register from './register'; import styles from './styles.module.scss'; class RegistrationForm extends Component { defaultFormState = { email: '', password: '', confirmPassword: '' } state = { form: { ...this.defaultFormState } } handleSubmit = (event) => { event.preventDefault(); const form = this.state.form; if(isFormValid(form)) { register(form); this.setState({ form: { ...this.defaultFormState }, areErrorsFound: false }) } else { this.setState({ areErrorsFound: true }) } } handleChange = (event) => { const name = event.target.name; const value = event.target.value; this.setState({ form: { ...this.state.form, [name]: value } }) } render() { const { areErrorsFound, form } = this.state; return ( <form onSubmit={this.handleSubmit} className={areErrorsFound ? styles.invalidForm : ''} > <input value={form.email} onChange={this.handleChange} name="email" type="email" placeholder="Email" /> <input value={form.password} onChange={this.handleChange} name="password" type="password" placeholder="Password" /> <input value={form.confirmPassword} onChange={this.handleChange} name="confirmPassword" type="password" placeholder="Confirm password" /> <button> Submit </button> </form> ) } } export default RegistrationForm; |
1 2 3 4 5 6 7 |
function isFormValid(form) { return form.password && form.confirmPassword && form.email && form.password === form.confirmPassword && /^.+@.+\..+$/.test(form.email); } |
1 2 3 4 5 |
.invalidForm { input { color: red; } } |
The regular expression that we use for an email is quite simple. While we might want to implement a more sophisticated way to do that, we shouldn’t overdo it. Even though popular email providers can have quite strict rules when it comes to email format, the RFC specification makes them quite flexible.
If you want to know more about regex in general, check out the regex course.
There are a few main things to notice above: I’ve written the isFormValid function to return a simple boolean. You might want to approach it differently and return a list of errors. Since it is a subjective thing, other developers might want to write it differently.
Another thing is the fact that the above code is a very simple example, yet it needed quite a lot of code. Handling different approaches, like validating on every change instead of a submit, would require us to write even more. Having a lot of code means that you need to test it thoroughly, and nobody is going to test it for you. The fact that every form in your – possibly big – application might differ does not help.
How Formik helps
Let’s rewrite the above form to use Formik.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
import React, { Component } from 'react'; import { Formik } from 'formik'; import register from './register'; import validateForm from './validateForm'; class RegistrationForm extends Component { defaultFormState = { email: '', password: '', confirmPassword: '' } handleSubmit = (form) => { register(form); } render() { return ( <Formik onSubmit={this.handleSubmit} initialValues={this.defaultFormState} validate={validateForm} > {({ handleSubmit, values, handleChange, handleBlur }) => ( <form onSubmit={handleSubmit} > <input value={values.email} onChange={handleChange} onBlur={handleBlur} name="email" type="email" placeholder="Email" /> <input value={values.password} onChange={handleChange} onBlur={handleBlur} name="password" type="password" placeholder="Password" /> <input value={values.confirmPassword} onChange={handleChange} onBlur={handleBlur} name="confirmPassword" type="password" placeholder="Confirm password" /> <button> Submit </button> </form> )} </Formik> ) } } export default RegistrationForm; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
function validateForm (form) { const errors = {}; if (!form.email) { errors.email = 'Required'; } if (!form.password) { errors.password = 'Required'; } if (!form.confirmPassword) { errors.confirmPassword = 'Required'; } if (form.password !== form.confirmPassword) { errors.confirmPassword = 'Password does not match'; } if (!/^.+@.+\..+$/.test(form.email)) { errors.confirmPassword = 'Invalid email'; } return errors; } |
In the code above, Formik did quite a bit work for us by handling the state of the form. It also standardized the way we validate data: now, our validation function returns an object. It is structured in a way that lets us know which input is an issue when validating. The form does not submit if there are any errors.
Even though Formik simplifies things a bit in the example above, there is still room for improvement. We have access to a set of components that can help us reduce the boilerplate and delegate even more of the work.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
import React, { Component } from 'react'; import { Formik, Form, Field } from 'formik'; import register from './register'; import validateForm from "./validateForm"; class RegistrationForm extends Component { defaultFormState = { email: '', password: '', confirmPassword: '' } handleSubmit = (form) => { register(form); } render() { return ( <Formik onSubmit={this.handleSubmit} initialValues={this.defaultFormState} validate={validateForm} > {() => ( <Form> <Field name="email" type="email" placeholder="Email" /> <Field name="password" type="password" placeholder="Password" /> <Field name="confirmPassword" type="password" placeholder="Confirm password" /> <button> Submit </button> </Form> )} </Formik> ) } } export default RegistrationForm; |
Diving deeper into form validation
Simply preventing submission of a form is not the only thing that Formik helps with in terms of validation. An important part of it is displaying the errors that occur.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
{({ errors }) => ( <Form> <Field name="email" type="email" placeholder="Email" /> <div>{errors.email}</div> <Field name="password" type="password" placeholder="Password" /> <div>{errors.password}</div> <Field name="confirmPassword" type="password" placeholder="Confirm password" /> <div>{errors.confirmPassword}</div> <button> Submit </button> </Form> )} |
The approach above is rather conventional. Therefore, Formik has a helper component for that also called ErrorMessage.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
{() => ( <Form> <Field name="email" type="email" placeholder="Email" /> <ErrorMessage name="email" component="div" /> <Field name="password" type="password" placeholder="Password" /> <ErrorMessage name="password" component="div" /> <Field name="confirmPassword" type="password" placeholder="Confirm password" /> <ErrorMessage name="confirmPassword" component="div" /> <button> Submit </button> </Form> )} |
All the form validation that we use above is straightforward, yet we need to write quite a bit of code for it to work. Another neat approach that we can implement is a validation schema. Instead of writing a function that validates the data, we can define an object that contains all the information needed for validation.
To create a validation schema, we need Yup. It is quite rich with different functions that aim to help you create a schema that you need. With it, we can describe our form in a way that Formik understands.
1 2 3 4 5 6 7 8 9 10 11 12 |
import * as Yup from 'yup'; const validationSchema = Yup.object().shape({ email: Yup.string() .email('Invalid email') .required('Required'), password: Yup.string() .required('Required'), confirmPassword: Yup.string() .required('Required') .oneOf([Yup.ref('password')], 'Password does not match') }); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
<Formik onSubmit={this.handleSubmit} initialValues={this.defaultFormState} validationSchema={validationSchema} > {() => ( <Form> <Field name="email" type="email" placeholder="Email" /> <ErrorMessage name="email" component="div" /> <Field name="password" type="password" placeholder="Password" /> <ErrorMessage name="password" component="div" /> <Field name="confirmPassword" type="password" placeholder="Confirm password" /> <ErrorMessage name="confirmPassword" component="div" /> <button> Submit </button> </Form> )} </Formik> |
Our validation schema is pretty straightforward. We define all fields as required and use a predefined regular expression for the email. The more exciting part is the confirmPassword field. To force it to be identical to the password field, we use the oneOf function to define a set of possible values. To get the current password, we use the ref function.
The Yup library contains many different functions that you can use. For a full list, check out the API documentation.
Summary
Here, we described how can we validate data on the front end, which is just a beginning. It is crucial to validate all the data on the backend also, even more strictly. You can never trust the data coming from your users because nothing can stop them from making a request to your API bypassing your interface. For example, they can do it with tools like Postman.
We’ve come quite a long way to refactor our React form with Formik. The most important thing that we’ve acquired is consistency. Thanks to that, every form that we do in our application works on the same principles, which make it easier to refactor, debug, and test. Also, there are quite a few tests written both for the Formik and Yup libraries unburdening you from that responsibility.