What I Learned at Work this Week: React Form Validation with Formik

Mike Diaz
7 min readApr 17, 2022

Formik is a React framework that makes our form-writing lives easier. I’m in the process of learning about it this week, but really I should have learned about it last week when an enthusiastic coworker offered to give me a crash course. I should have understood that their enthusiasm was a reflection of how helpful the knowledge would be, but instead I was so focused on my other problems that I took the tutorial for granted. After apologizing for my behavior, I asked if I could look at some of their work to try and understand all the excitement. Today, we’ll review a React form that uses Formik and see what advantages it has over classic React form structure.

Formik

Formik is not the only form helper library for React. According to its documentation, Formik separates itself from alternatives by focusing on three common challenges:

  1. Getting values in and out of form state
  2. Validation and error messages
  3. Handling form submission

Formik facilitates these tasks with less space and more computing efficiency than something like Redux-Form. So how does it work?

Handling State

In a 2018 presentation at React Alicante, Jared Palmer reminded his audience of the traditional means of managing state in a React form:

handleInputChange(event) {
const target = event.target;
const value = target.type === 'checkbox' ?
target.checked : target.value;
const name = target.name;
this.setState({
[name]: value,
});
}

We would expect to invoke this function onChange for our form input fields. When someone types into the form (or toggles a checkbox), the state of that field will be updated with the new value of the input. Palmer calls out a couple of drawbacks:

  1. State is constantly being updated. If I understand correctly, Formik doesn’t strictly solve this, but it’s much more lightweight than alternatives, especially those that use Redux.
  2. This boilerplate code has to be added to component after component, obscuring the unique, relevant logic.

Formik abstracts this logic and applies it by using a render prop (a React technique for sharing code between components). Palmer’s demo render prop, called MiniFormik because it is a simplified version of how his library works, starts like this:

class MiniFormik extends React.Component {
state = {};
render() {
return this.props.children({ ...this.state });
}
}

We initially declare MiniFormik to be a class which inherits all the properties of a classic React Component. When instantiated, it will have a state property (currently an empty object) and a render function. I spent a long time trying to understand what’s going on inside that function because my understanding is that props.children is usually a collection, like an array or object, but here it’s being used as a function.

I couldn’t find an example of this.props.children being used as a function until I remembered that MiniFormik is supposed to be a render prop. The idea here is that the children are functional React components. The render function is going to invoke them, passing along the state we define.

This might be easier to understand as we flesh out the code. After building this basic render prop, Palmer adds in the logic we’re trying to abstract. We’ll no longer have to add this to each individual component — it’ll be available in all of the children rendered by MiniFormik:

class MiniFormik extends React.Component {
state = {
values: this.props.initialValues || {},
touched: {},
errors: {},

};
handleChange = (event) => {
const target = event.target;
const value = target.type === 'checkbox' ?
target.checked : target.value;
const name = target.name;
this.setState({
[name]: value,
});
}
render() {
return this.props.children({
...this.state,
handleChange: this.handleChange
});
}
}

To complete the propagation, we add handleChange as a prop for the children we render. Now it’s more clear why this.state uses a spread operator: because we’re passing an object which contains state, but also includes new key-value pairs that we’ll define.

Speaking of state, we‘ve also made some changes there. Palmer adds three keys: values, touched, and errors. We can imply/guess what these will eventually look like, but for now what we know for sure is that values will be defined by props passed during invocation. Let’s take our first look at the component that is using MiniFormik:

class Reservation extends React.Component {
render() {
return (
<MiniFormik
initialValues={{
isGoing: true,
numberOfGuests: 2
}}
>

{() => (
<form>
<input
name="isGoing"
type="checkbox"
className="checkbox"
checked={this.state.isGoing}
onChange={this.handleChange}
/>
...

This is what a render prop looks like in practice. MiniFormik wraps a function that returns a more traditional form with inputs and labels. This form is a checkbox that indicates whether someone isGoing or not. MiniFormik is instantiated with initialValues, an object that has isGoing and numberOfGuests properties. If we attempt a render at this point, we’ll get an error because this.state.isGoing and this.handleChange are undefined. We have to pass these into the component function as arguments:

class Reservation extends React.Component {
render() {
return (
<MiniFormik
initialValues={{
isGoing: true,
numberOfGuests: 2
}}
>
{({ values, handleChange }) => (
<form>
<input
name="isGoing"
type="checkbox"
className="checkbox"
checked={values.isGoing}
onChange={handleChange}
/>
...

But there’s still a problem here: the setState we dropped into handleChange won’t work with our new setup.

this.setState({
[name]: value,
});
}

Remember that our state looks like this:

state = {
values: this.props.initialValues || {},
touched: {},
errors: {},
};

setState can’t directly update isGoing because it’s not a top-level key. Instead, we have to update the use of setState to more specifically alter the values object:

event.persist();
this.setState(prevState => ({
values: {
...prevState,
[name]: value,
},
}));

I included event.persist for anyone following along in the video. That syntax, which prevents our event data from being reset when SyntheticEvents are pooled (an efficiency optimization), is no longer necessary thanks to updates in React 17. When Jared Palmer gave his presentation, omitting this code would have cause dour code to break when we attempted to reference prevState.

By adding prevState, we can maintain all the properties of values that we don’t want to be affected by our change and alter only isGoing, for example. Finally, the code is working.

What’s the big deal?

What we see in this initial example is an upgrade on the way React was commonly written in 2018, but it doesn’t look too special today. It’s probably something we might have done in one of our homemade apps without needing any imported library, so why should we be excited about Formik? First of all, the benefit of the library is that we can import components that use this code (and more) rather than having to define them ourselves. But Formik also gives us a big boost when it comes to validation.

In his demo, Palmer paired Formik with a validation library called Yup (which is perfect because we use the same pattern at my workplace). In the “Tutorial” section of the Formik docs, there is also an example that uses Yup:

import React from 'react';
import { useFormik } from 'formik';
import * as Yup from 'yup';

const SignupForm = () => {
const formik = useFormik({
initialValues: {
firstName: '',
lastName: '',
email: '',
},
validationSchema: Yup.object({
firstName: Yup.string()
.max(15, 'Must be 15 characters or less')
.required('Required'),
lastName: Yup.string()
.max(20, 'Must be 20 characters or less')
.required('Required'),
email: Yup.string().email('Invalid email address').required('Required'),
}),
onSubmit: values => {
alert(JSON.stringify(values, null, 2));
},
});

We’ve now moved on from defining a simplified version of Formik to importing the library itself. Before returning our JSX elements, we define a formik object which contains one of the properties we’ve familiarized ourselves with: initialValues. We also see two new properties:

  • onSubmit: We’ll skip to the bottom since this one is simpler. This function contains the logic to run when a form is submitted and will be passed as a prop further along in the code. In this case, it just generates an alert in the browser and shows values from our state as JSON. If you’ve never used more than one argument in JSON.stringify…don’t feel too bad because neither have I. Per MDN, the second argument is a replacer which can filter or alter our results. The third is just a value for space, which indicates how far we should indent the results.
  • validationSchema: Here we’re using Yup to describe what we consider a valid object. It must contain firstName, lastName, and email properties. The values associated with these properties describe their data types and lengths. The strings inside the validator functions, like 'Required', are the messages that can be displayed for invalid fields.

Finally, the JSX implementation is just as simple:

return (
<form onSubmit={formik.handleSubmit}>
<label htmlFor="firstName">First Name</label>
<input
id="firstName"
name="firstName"
type="text"
onChange={formik.handleChange}
onBlur={formik.handleBlur}
value={formik.values.firstName}
/>
{formik.touched.firstName && formik.errors.firstName ? (
<div>{formik.errors.firstName}</div>

) : null}
...

handleSubmit is just passed as a prop value to the form and then we see our validator referenced after our elements. That bolded ternary is checking whether the firstName input has been touched (no need to display an error before someone interacts with an element) and whether Yup has returned any validation errors based on our schema. If so, display the error message (which is saved in Formik’s state — remember earlier when we added an errors object there?) in a div.

Timing is everything

Formik is really cool and I’m curious to see how it’s implemented in my team’s codebase. I wasn’t ready to take on the new knowledge when it was first presented to me and I should have expressed that instead of trying to power through. Real learning requires the right environment and mindset — putting ourselves in a difficult situation doesn’t help anyone.

Sources

--

--