About this post
In the previous post of this series, we built a form using React only. We used React's state
to store the values held by each <input>
, making them React-controlled components. The resulting form, which was functional for our purposes, included a lot of boilerplate. But, we can do much better!
We're going to introduce a library called Formik that should hopefully make building forms much less painful.
Adding Formik to the Project
To add Formik to our project, we will use the npm install
command, as we did for Bootstrap in the last post.
$ npm install --save formik
And make sure that formik
is now in your list of dependencies.
/* Part of package.json */
"dependencies": {
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2",
"bootstrap": "^4.4.1",
"formik": "^2.1.4", /* Make sure you see this line */
"react": "^16.12.0",
"react-dom": "^16.12.0",
"react-scripts": "3.4.0"
},
Rewriting LoginForm
In the original version of LoginForm
, we had a lot of boilerplate involved. We needed to set up state
to handle current form state, form validation and error messages. In Formik, there is built-in support for handling state. Validation will still be specified by us, but Formik has a prop we can set for this validation function.
We're not going to duplicate the entire LoginForm
class from the previous post, but we should touch on its interface. The component had the following methods, with summaries of what they did:
// Class structure for LoginForm
class LoginForm extends React.Component {
constructor(props) {
/* In this method we initialized `state` for the form values,
the field validity, and their errors. */
...
}
onSubmit(event) {
/* Iterated over the form values and checked if they were valid */
...
}
validate(name, value) {
/* Checked if a given value was valid, based on the field name */
...
}
onChange(event) {
/* Because this is a controlled component, we need to update our
form values whenever they change */
...
}
render() {
/* The HTML for our component */
...
}
}
By using Formik, we no longer need to do our own onChange
updating. The validation step is handled by Formik, so we don't need to add that to our onSubmit
method. Finally, Formik handles initial values, stores validity, and lets the user check errors via a validate
method.
Rewriting our LoginForm
using Formik will make building this form much less complicated, and much more routine.
Import Formik
In order to use Formik, we need to import it. Here's what the top of LoginForm.js
should look like.
// Top of LoginForm.jsx
import React from 'react';
import { Formik, Form, Field, ErrorMessage } from "formik";
class LoginForm extends React.Component {
...
}
Rewriting render
We're going to use our new Formik
component to rewrite the render
method of our LoginForm
. The main <form>
tag will be replaced by <Formik>
. In turn <Formik>
is passed a function that renders the <Form>
. Note that the 'F' is capitalized, since this component is specific to Formik.
The <Formik>
component requires a few props be set to be useable:
-
initialValues
- Determines the initial state of the form. -
validate
- A function which validates the form, and updates any errors. -
onSubmit
(optional) - A function we want to call after validation, but before final submit. This is likely where you would send your payload to the HTTP server.
Comments are given inline, pointing out important usage of <Form>
, <Field>
, and <ErrorMessage>
.
class LoginForm extends React.Component {
...
render() {
return (
<div className="container">
<div className="row justify-content-center">
<div className="col-lg-6">
<div className="col-lg-12">
/* Add new <Formik> component with two new methods that we have
not written yet: `initialValues` and `validate` */
<Formik
initialValues={this.initialValues()}
validate={this.validate.bind(this)}>
{
props => (
/* Our <Form> component is our main container */
<Form>
<div className="form-group">
<label htmlFor="email">Email</label>
/* This <Field> handles state change for the <input> */
<Field
type="email"
name="email"
placeholder="Enter email"
className={`form-control ${props.errors.email ? "is-invalid" : ""}`}
/>
/* Formik handles error messages for us with this component. */
<ErrorMessage
component="div"
name="email"
className="invalid-feedback"
/>
</div>
/* The changes to the password field are similar */
<div className="form-group">
<label htmlFor="password">Password</label>
<Field
type="password"
name="password"
placeholder="Enter password"
className={`form-control ${props.errors.password ? "is-invalid" : ""}`}
/>
<ErrorMessage
component="div"
name="password"
className="invalid-feedback"
/>
</div>
<button type="submit" className="btn btn-primary btn-block">
Log in
</button>
</Form>
)
}
</Formik>
</div>
</div>
</div>
</div>
);
}
}
Adding initialValues
and validate
The biggest change to our form is in the render
method. We are close to done with our rewrite, but we still have a two more methods: initialValues
and validate
. Below are the implementations that should work for our needs:
class LoginForm extends React.Component {
initialValues() {
return {
email: "",
password: ""
}
}
validate(values) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/i;
let errors = {};
if (values.email === "") {
errors.email = "Email is missing";
} else if (!emailRegex.test(values.email)) {
errors.email = "Email is not in the expected email address standard format";
}
if (values.password === "") {
errors.password = "Password is missing";
} else if (values.password.length < 6) {
errors.password = "Password must be 6 characters at minimum";
}
return errors;
}
...
}
The initialValues
method returns a new JavaScript object with empty strings for email
and password
. The validate
method has changed to taking a JavaScript object with the current form values. We handle not only the two previous validations from our React-only form, but also verify that these fields are not empty, letting the user know they are missing.
We're now ready to test our the refactored form.
Testing it Out
After making those changes, we should have a working login page again.
When using this form, you'll notice that the error message for email appears immediately after switching from email to password. Also, we are checking for multiple validations, not just required or email format.
Our new form has the same functionality as the previous React form, which means our refactor was successful! However, we can go further. In particular, the validate
method can be refactored further. Formik has built in support for another library called Yup, which allows us to describe the fields of our form in a declarative way.
Using Yup for Validation
Before we get into what Yup can do, let's first add it to our package.json
.
$ npm install --save yup
Verify you have the correct package:
/* Part of package.json */
"dependencies": {
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2",
"bootstrap": "^4.4.1",
"formik": "^2.1.4",
"react": "^16.12.0",
"react-dom": "^16.12.0",
"react-scripts": "3.4.0",
"yup": "^0.28.1" /* Make sure you see this line */
}
Now let's import it into our project.
// Top of LoginForm.jsx
import React from 'react';
import { Formik, Form, Field, ErrorMessage } from "formik";
import * as Yup from 'yup'; /* Add this line to */
class LoginForm extends React.Component {
...
}
The new Yup
object we've imported has the ability to create JSON schemas via the object
method. Let's add this code just above our LoginForm
:
import * as Yup from 'yup';
/* Add this new schema */
const loginFormSchema = Yup.object().shape({
email: Yup.string()
.email("Email is not in the expected email address standard format")
.required("Email is missing"),
password: Yup.string()
.required("Password is required")
.min(6, "Password must be 6 characters at minimum")
});
class LoginForm extends React.Component {
...
}
We're going to get rid of the validate
prop of the Formik
component, but we're going to add a validateSchema
prop, to use the new schema:
class LoginForm extends React.Component {
...
render() {
return (
<div className="container">
<div className="row justify-content-center">
<div className="col-lg-6">
<div className="col-lg-12">
<Formik
initialValues={this.initialValues()}
/* New prop validateSchema */
validateSchema={loginFormSchema}>
...
</Formik>
</div>
</div>
</div>
</div>
);
}
}
Let's test our form, to confirm it still works properly.
Success! Our LoginForm
component now uses Formik for it's <input>
fields and Yup for defining the validations.
Conclusion
React is a very useful framework for building interactive websites. However, because React must control all state on the page, we cannot use vanilla DOM elements. To provide a user experience that most customers expect of web forms, we can use Formik to handle common needs, including validation. We refactored our hand-written validate
method to use Yup, which allows us to declaratively describe our form.
Top comments (0)