Building dynamic forms with Formik with React and TypeScript
Sep 1, 2018Building forms has always been one of React's weaknesses. The unidirectional data flow paradigm in React doesn't mesh with the ephemeral-ness of HTML forms. To get around this, we'd need to leverage React's local state and update that state with the value of the form whenever an input was changed. This made maintaining forms difficult, tedious, and unscalable.
Enter Formik: https://github.com/jaredpalmer/formik
We'll use Formik to help cover business cases and how it can make building dynamic forms that are validated all on their own incredibly simple and easy. For this post, I have a git repo that will be split in multiple branches to cover each case for brevity and conciseness. You can clone it here.
Case #1: Initial form
Let's write a basic form with Formik. No styles, just a multitude of different fields and a submit button. We can log out all the values we have. Let's do that:
import React from "react";
import { Formik } from "formik";
const initialValues = {
firstName: "",
pet: "",
};
const App: React.SFC = () => (
<>
<h1>Working with Formik</h1>
<Formik initialValues={initialValues}
onSubmit={(values) => console.log(values)}
render={({ handleSubmit, handleChange }) => (
<form onSubmit={handleSubmit}>
<label htmlFor="firstName">
<div>First Name</div>
<input type="text" name="firstName"/>
</label>
<label htmlFor="pet">
<div>Pet</div>
<select name="pet" onChange={handleChange}>
<option>Dog</option>
<option>Cat</option>
<option>Other</option>
</select>
</label>
<button type="submit">Submit</button>
</form>
)}>
</Formik>
</>
);
export default App;
Here we have a really basic HTML form with helpers provided to us by Formik. However it's pretty tedious to have to constantly add the name
and onChange
props to all our fields. Let's make it so that we don't need to do that, leveraging Formik's Field
component. It automatically wires up those props for you, taking in only the name
as a prop.
import React from "react";
import { Formik, Field } from "formik";
const initialValues = {
firstName: "",
pet: "",
};
const App: React.SFC = () => (
<>
<h1>Working with Formik</h1>
<Formik initialValues={initialValues}
onSubmit={(values) => console.log(values)}
render={({ handleSubmit }) => (
<form onSubmit={handleSubmit}>
<label htmlFor="firstName">
<div>First Name</div>
<Field type="text" name="firstName"/>
</label>
<label htmlFor="pet">
<div>Pet</div>
<Field name="pet" component="select">
<option value="Dog">Dog</option>
<option value="Cat">Cat</option>
<option value="Other">Other</option>
</Field>
</label>
<button type="submit">Submit</button>
</form>
)}>
</Formik>
</>
);
export default App;
Aside: Here, we can use the <Form/>
helper to replace our <form onSubmit={handleSubmit}/>
tag, but for the sake of simplicity we'll leave the code as is.
Note: The git repo will have the most up-to-date code in the app.tsx
file, but each branch will contain files for code examples in this post to make it easier to reference.
Case #2: Form validation
Now that we have our form, let's try to add some validation to it. Luckily for us, Formik provides a hook for that. Let's check out the case/form-validation
branch and continue on from there.
Let's add some validation to our form and make entering your first name required.
<Formik initialValues={initialValues}
onSubmit={(values) => console.log(values)}
validate={(values) => {
const errors = {
firstName: null,
};
if (!values.firstName) {
errors.firstName = "Required";
}
return errors;
}}
// render the rest of the component...
We can add validation on a per-field basis, mapped to the name
of the input. The validate
prop via Formik will fire this function every time the form is submitted. If there are errors, then it will return an errors
object. If there are no fields inside of the errors
object, then the form will be submitted.
Suppose we want to type our form values? With TypeScript we can create an interface for our form values to take advantage of static types!
interface FormValues {
firstName: string;
pet: string;
}
We'll also import the FormikErrors
type from the Formik package and type the rest of our component:
import React from "react";
import { Formik, Field, FormikErrors } from "formik";
export interface FormValues {
firstName: string;
pet: string;
}
const initialValues: FormValues = {
firstName: "",
pet: "",
};
const App: React.SFC = () => (
<>
<h1>Working with Formik</h1>
<Formik initialValues={initialValues}
onSubmit={(values: FormValues) => console.log(values)}
validate={(values: FormValues) => {
const errors: FormikErrors<FormValues> = {};
if (!values.firstName) {
errors.firstName = "Required";
}
return errors;
}}
// render the form...
Presto! Now we have static types that make it easy for other developers to see what the form data will return.
However, suppose you have a lot of fields? It'd be tedious to write a custom validation function for every field. Let's use Yup for our form validation.
Note: at this point in time, the code will be saved in another file called ValidatedForm.tsx
and we'll update our app.tsx
file.
import { string, object } from "yup";
const App: React.SFC = () => (
<>
<h1>Working with Formik</h1>
<Formik initialValues={initialValues}
onSubmit={(values: FormValues) => console.log(values)}
validationSchema={object().shape({
firstName: string().required(),
})}
// render the form...
Here we got rid of our custom validate
function, and used a custom schema via Yup. What if we want to return a specific error message? We can add a string as a param for all of yup's validation:
firstName: string().required("Entering your first name is required.")
This string will be returned via Formik's errors
object if the schema fails. Let's add some markup to display the error to the user:
// Formik component...
render={({ handleSubmit, errors, touched }) => (
<form onSubmit={handleSubmit}>
<label htmlFor="firstName">
<div>First Name</div>
<Field type="text" name="firstName"/>
{
touched.firstName && errors.firstName
? <div>{errors.firstName}</div>
: null
}
</label>
// the rest of the form...
As you can see, Formik gives us a lot of tools to work with. Run the application and try to submit an empty firstName
field. We'll see an error message pop up just underneath the field, and the form will not be submitted.
In summary, using Yup with Formik provides an easy experience to creating forms and validating them. This takes out most of the legwork when creating forms in React, allowing developers to focus on building real forms, not fighting form validation or form creation.
Next, we'll create custom styled fields and spice up our form using styled-components! Check out the case/styled-fields
branch.
Case #3: Custom styled fields
The idea behind having custom fields is that we can leverage the power of React to compartmentalize our responsibilities. Let's make a custom React component that will render a Formik Field
and also handle the rendering of any error messages.
// TextField.tsx
import React from "react";
import { Field, FieldProps } from "formik";
import { FormValues } from "./app";
interface Props {
title: string;
name: string;
}
type OwnProps = FieldProps<FormValues> & Props;
const TextField: React.SFC<OwnProps> = ({ name, title, field, form }) => (
<label htmlFor={name}>
<div>{title}</div>
<Field type="text" name={name}/>
{
form.touched[name] && form.errors[name]
? <div>{form.errors[name]}</div>
: null
}
</label>
);
export default TextField;
I exported the FormValues interface so we can use it here. Now we can specify a TextField
in our form:
import React from "react";
import { FieldProps } from "formik";
import { FormValues } from "./app";
interface Props {
title: string;
}
type OwnProps = FieldProps<FormValues> & Props;
const TextField: React.SFC<OwnProps> = ({ title, field, form }) => (
<label htmlFor={field.name}>
<div>{title}</div>
<input type="text" {...field}/>
{
form.touched[field.name] && form.errors[field.name]
? <div>{form.errors[field.name]}</div>
: null
}
</label>
);
export default TextField;
Then back in our form:
// App...
<Field name="firstName" render={(innerProps) => (
<TextField {...innerProps} title="First Name"/>
)}/>
// render the rest of the form...
Now we have a reusable text field component that isn't tightly coupled to only a single field. Now we can add multiple text fields to our form, while having each form field maintain its own validation and styles. Let's go ahead and add those styles now:
import styled from "styled-components";
const Title = styled.h2`
font-family: Arial, Helvetica, sans-serif;
color: cornflowerblue;
margin-bottom: 5px;
`;
const FormInput = styled.input`
border-color: gray;
border: 1px solid;
padding: 5px 5px;
margin-bottom: 5px;
`;
const InputError = styled.div`
color: red;
font-family: Arial, Helvetica, sans-serif;
margin: 15px 0;
`;
Adding them to our component:
// TextField.tsx
const TextField: React.SFC<OwnProps> = ({ title, field, form }) => (
<label htmlFor={field.name}>
<Title>{title}</Title>
<FormInput type="text" {...field}/>
{
form.touched[field.name] && form.errors[field.name]
? <InputError>{form.errors[field.name]}</InputError>
: null
}
</label>
);
Now we have a snazzy looking text field that's easy to maintain both field logic and styles all in one component.
Case #4: Array of Fields
By now you'll be asking me: "But Scott, I just wanna map over these same fields and be done with my life and finally go outside." Boy do I have something for you! Formik ships with a helper React component called <FieldArray/>
. This is a helper component that handles an array of the same type of fields. Suppose we have not one person, but three people that we want to know their first names and their preferred pet:
First, we'll need to update our initialValues:
const initialValues: FormValues = {
people: [
{
firstName: "",
pet: "",
},
{
firstName: "",
pet: "",
},
{
firstName: "",
pet: "",
},
],
};
And we'll have a type error, so we need to fix that:
export interface FormValues {
people: Array<{
firstName: string;
pet: string;
}>;
}
And update our Yup schema:
validationSchema={object().shape({
people: array().of(object().shape({
firstName: string().required("Entering a first name is required"),
})),
})}
Then we have our new form, with an array of duplicated fields!
const App: React.SFC = () => (
<>
<h1>Working with Formik</h1>
<Formik initialValues={initialValues}
onSubmit={(values: FormValues) => console.log(values)}
validationSchema={object().shape({
people: array().of(object().shape({
firstName: string().required("Entering a first name is required"),
})),
})}
render={({ handleSubmit, errors, touched, values }) => (
<Form>
<FieldArray name="people"
render={(helpers) => (
<div>
{values.people && values.people.length > 0 ? (
values.people.map( (person, index) => (
<React.Fragment key={index}>
<Field name={`people.${index}.firstName`} render={(innerProps) => (
<TextField {...innerProps} title="First Name"/>
)}/>
<label htmlFor="pet">
<div>Pet</div>
<Field name={`people.${index}.pet`} component="select">
<option value="Dog">Dog</option>
<option value="Cat">Cat</option>
<option value="Other">Other</option>
</Field>
</label>
</React.Fragment>
))
) : null}
<button type="submit">Submit</button>
</div>
)}/>
</Form>
)}>
</Formik>
</>
);
But when we submit it, we need to update our TextField
component to recognize which error to render:
// TextField.tsx
const TextField: React.SFC<OwnProps> = ({ title, field, form, index }) => (
<label htmlFor={field.name}>
<Title>{title}</Title>
<FormInput type="text" {...field}/>
{
form.errors.people !== undefined
&& form.errors.people[index] !== undefined
&& form.touched.people !== undefined
&& form.touched.people[index] !== undefined
? <InputError>{form.errors.people[index].firstName}</InputError>
: null
}
</label>
);
This will now allow us to have an array of text fields that have per-field validation! Using Yup with Formik is immensely powerful, as we can specify a single schema and leverage that across all of our form fields.
Summary
Formik is a fantastic way to compartmentalize smaller form chunks into their own field-level components, while allowing the parent form to control the validation of each form input according to a schema. Adding forms this way makes it much easier to maintain React forms because the child fields only need to render the field and the error messages if there is one. Each child field maintains that singular responsibility, while the parent form maintains the validation, submission, and rendering of children. Suppose we want to update our fields to add some sort of analyitics? It's trivial to do so, as we can update our TextField.tsx
file.
As always, pull requests and comments are greatly appreciated: https://github.com/scottdj92/forms-with-formik.