Managing nested forms gracefully with Formik
Large forms needn’t be complex or hard
The toughest part of building a React app is managing forms. No questions about it. Simple presentational components are easy to build. But forms.. You have so many concerns to worry about. Asynchronous user input. Different input mechanisms, validations, data transformations, initial default values.. It never ends.
I this article, I’ll demonstrate a way to wrangle large forms. How to split a form into smaller compose-able chunks. We’ll use the formik npm package to reduce boilerplate. Formik has a bit of magic code. I don’t like magic. In this case, the benefits outweigh my distaste for magic.
React boiler plate
Here is a simple React form without any support libraries:
export default AppForm = () => {
const [value, setValue] = React.useState();const handleChange = event => {
setValue(event.target.value);
};const handleSubmit = event => {
alert("A name was submitted: " + value);
event.preventDefault();
};return (
<form onSubmit={handleSubmit}>
<label>
Name:
<input type="text" value={value} onChange={handleChange} />
</label>
<input type="submit" value="Submit" />
</form>
);
}
What you immediately see is the enormous amount of boilerplate code that you need to write just to get a simple input field working. This will never scale.
Step 1. Start with Formik
Formik
One library we have been using to help us reduce the boilerplate is Formik. There is a bit of learning curve to understand Formik but once you get a hang of it, Formik lets you write less code to build all kind of forms. Formik integrates very nicely with another package called yup. A Formik managed React form contains these distinct parts:
- An initial set of values.
- A schema to validate the form via yup
- The form itself containing fields
All of these are linked via the field names. Here is how I handle a simple form:
import React from "react";
import { Formik, Form, Field } from "formik";
import * as Yup from "yup";const initialValues = {
firstName: ""
};const nameSchema = Yup.object().shape({
firstName: Yup.string().required("Required")
});const NameForm = ({ errors, touched, handleSubmit }) => (
<Form>
<label>
Name:
<Field name="firstName" />
<input type="submit" value="Submit" onSubmit={handleSubmit} />
{errors.firstName && touched.firstName ?
<div>{errors.firstName}</div> : null
}
</label>
</Form>
);const handleSubmit = values => {
console.log(values);
};export const ReactFormikForm = () => (
<div>
<h3>Formik Form with validation</h3>
<Formik
initialValues={initialValues}
validationSchema={nameSchema}
onSubmit={handleSubmit}
children={NameForm}
/>
</div>
);
See how the field is linked magically within the form, initialValues and nameSchema? At first it looks like a lot of code just to handle a simple form. But the recipe is in place, you can now add fields incrementally each with its own validation. For example, to adding another field would look like this:
import React from "react";
import { Formik, Form, Field } from "formik";
import * as Yup from "yup";const initialValues = {
firstName: "",
lastName: ""
};const nameSchema = Yup.object().shape({
firstName: Yup.string().required("Required"),
lastName: Yup.string().required("Required")
});const handleSubmit = values => {
console.log(values);
};const NameForm = ({ errors, touched, handleSubmit }) => (
<Form>
<label>
First Name:
<Field name="firstName" />
{errors.firstName && touched.firstName ? (
<div>{errors.firstName}</div>
) : null}
</label>
<label>
Last Name:
<Field name="lastName" />
{errors.lastName && touched.lastName ? (
<div>{errors.lastName}</div>
) : null}
</label>
<input type="submit" value="Submit" onSubmit={handleSubmit} />
</Form>
);export const ReactFormikForm = () => (
<div>
<h3>Formik Form with validation</h3>
<Formik
initialValues={initialValues}
validationSchema={nameSchema}
onSubmit={handleSubmit}
children={NameForm}
/>
</div>
);
The largest additional block of code is the new form element itself. But everything else stays in place. For simple forms, this is more than enough. But what happens when you have 10–20 fields to be presented in a single form or even distributed with in a wizard? To get to that we need to first figure out how to create a custom field in Formik.
You can see all of this in action here:
Step 2. A custom field
The out of the box version of Formik only provides you with the basic HTML form components. There are several examples out there to help you with managing the basic form elements. What we want to create is a non standard Form field. We are going to build a traffic light. :-)
A traffic light component.
This component will display three lights. You can initialize the component with a color. You may also click on a color to change it. We will also use styled components library. There is a bit of learning but they keep your components very clean.
import React from "react";
import styled from "styled-components";const Light = styled.div`
height: ${props => props.size};
width: ${props => props.size};
background-color: ${props => props.color};
border-radius: 50%;
border: 1px solid grey;
display: inline-block;
margin: 5px;
`;const LightGroup = styled.div`
width: ${props => props.size};
padding: 10px;
display: flex;
flex-direction: column;
justify-content: space-between;
border: 1px solid grey;
border-radius: 8px;
background-color: #efefef;
`;export const TrafficLight = () => {
const colors = ["red", "orange", "green"];
const [current, setCurrent] = React.useState(0);
const colorFor = index => (current === index ? colors[index] : "transparent");return (
<LightGroup size="50px">
<Light size="40px" color={colorFor(0)} onClick={() => setCurrent(0)} />
<Light size="40px" color={colorFor(1)} onClick={() => setCurrent(1)} />
<Light size="40px" color={colorFor(2)} onClick={() => setCurrent(2)} />
</LightGroup>
);
};
You can ignore the styled definitions and go straight to the Component. It manages a state called current. What we now need to do is provide three inputs to this Component to convert it into a Formik component:
- name - name of this field
- value - the initial value
- setFieldValue - A function that will be wired in by Formik that lets the component set the field’s value.
setFieldValue looks like this:
setFieldValue?: (name: string, value: any, shouldValidate?: boolean) => void;
We’re only going to look at the name and value arguments.
Enhance the component like so:
let TrafficLightField = ({ name, value, setFieldValue }) => {
const colors = ["red", "orange", "green"];
const [current, setCurrent] = React.useState(value);
const colorFor = index => (current === index ? colors[index] : "transparent");React.useEffect(() => {
name && setFieldValue && setFieldValue(name, current);
}, [name, current, setFieldValue]);return (
<LightGroup size="50px">
<Light size="40px" color={colorFor(0)} onClick={() => setCurrent(0)} />
<Light size="40px" color={colorFor(1)} onClick={() => setCurrent(1)} />
<Light size="40px" color={colorFor(2)} onClick={() => setCurrent(2)} />
</LightGroup>
);
};
You’ve basically created a useEffect hook that updates the form field’s value anytime the current value changes. Now we have to wire this with Formik via a HOC:
const withField = Component => ({ field, form, ...props }) => (
<Component {...field} {...form} {...props} />
);TrafficLight = withField(TrafficLight);
That’s it. I know it is a bit of mental gymnastics to get your head wrapped around an HOC. But they let you separate the Formik logic out of your component, making it available for other uses. Essentially withField returns the Component with addition properties added to it. In out case it results in something like this:
<TrafficLight name={field.name} value={field.value} setFieldValue={form.setFieldValue} />
Here is the codedandbox that has the new Component tacked on. The form looks very ugly. But we’re not here for prettyfying a form, right?
Okay. next step.
Step 3. Sub Forms
With the same concept as a custom field, we’ll create forms that are essentially Formik fields themselves. The secret lies in the setFieldValue function passed in via Formik. We just need to connect the necessary wires. Instead of the useField HOC, we’ll build a useForm HOC. Let us build a sub-form that handles two fields, first name and last name.
First, let us move the first and last name fields to a separate form:
import React from "react";
import { Form, Field } from "formik";
import * as Yup from "yup";
import { withSubForm } from "./withSubForm";
import { useValues } from "./useValues";const nameSchema = Yup.object().shape({
first: Yup.string().required("Required"),
last: Yup.string().required("Required")
});const NameForm = ({ name, errors, touched, ...props }) => {// insert the hook once we are done defining it
// useValues(name, props); return (
<Form>
<label>
First Name:
<Field name="first" />
{errors.first && touched.first ? <div>{errors.first}</div> : null}
</label>
<label>
Last Name:
<Field name="last" />
{errors.last && touched.last ? <div>{errors.last}</div> : null}
</label>
</Form>
);
};
If you notice, it looks pretty much the same. There are two fields with Yup validations. Just as in the single form example before. But this form does nothing outside itself. So we need to add some hooks to update the field’s values. We will build a custom hook for that.
import { isEmpty } from "ramda";
import React from "react";// Anytime the value changes, set the field value
export const useValues = (name, props) => {
React.useEffect(() => {
if (!isEmpty(props.errors)) {
props.setFieldError(name, "SubFormError");
}
}, [name, props.values]);
};
That commented line in NameForm can be used now.
This form is still not ready to be plugged in. Something has to supply the necessary form and field properties to it. We will create an HOC for this:
export const withSubForm = (SubForm: React.ElementType, validationSchema: any) => ({field, form, fieldProps } => {
const initialValues = field.value; return (
<Formik initialValues={initialValues} validationSchema={validationSchema} onSubmit={onSubmit} children={props => (
<SubForm
{...props}
setFieldValue={form.setFieldValue}
name={field.name}
fieldProps={fieldProps}
/>
)}
/>
);
};
Now export the HOC wrapped component:
export const NameSubForm = withSubForm(NameForm, nameSchema);
NameSubForm can be now used as a field.
<Field name="name" component={NameSubForm} />
It produces a JSON object as its field value. Now your main Formik form values will look like this:
{
name: {
first: "",
last: ""
}
}
HOCs can be hard to reason in your head. We have also not taken care of field error handling. That only involves a few more lines of code. You can try it here:
https://codesandbox.io/s/react-formik-yup-sub-forms-mekfp
Hope this tutorial helps you in wrangling large nested forms by composing them as small subForms within Formik.