Post contents
React is a powerful library for building user interfaces, but it doesn't come with a built-in way to handle forms. While there are many libraries that can help supplement this issue, it can be difficult to know which one to choose.
One such option for form handling is Formik comes in. Formik is a popular library that simplifies form handling in React, providing a set of tools to manage form state, validation, and submission.
Here's what a basic form might look like in Formik:
import { useFormik } from "formik";const FormComponent = () => { /** * Formik provides us a hook called "useFormik" which allows us to * define the initial values and submitted behavior * * This return value is then used to track form events and more */ const formik = useFormik({ initialValues: { name: "", email: "", }, onSubmit: (values) => { console.log(values); }, }); return ( <form onSubmit={formik.handleSubmit}> <div> <label> Name <input type="text" name="name" onChange={formik.handleChange} value={formik.values.name} /> </label> </div> <div> <label> Email <input type="text" name="email" onChange={formik.handleChange} value={formik.values.email} /> </label> </div> <button type="submit">Submit</button> </form> );};Basic Form - StackBlitz
Editimport { useFormik } from "formik";const FormComponent = () => { /** * Formik provides us a hook called "useFormik" which allows us to * define the initial values and submitted behavior * * This return value is then used to track form events and more */ const formik = useFormik({ initialValues: { name: "", email: "", }, onSubmit: (values) => { console.log(values); }, }); return ( <form onSubmit={formik.handleSubmit}> <div> <label> Name <input type="text" name="name" onChange={formik.handleChange} value={formik.values.name} /> </label> </div> <div> <label> Email <input type="text" name="email" onChange={formik.handleChange} value={formik.values.email} /> </label> </div> <button type="submit">Submit</button> </form> );};export default FormComponent;<Formik/> Component
The useFormik hook isn't the only way to declare a form, however. Formik also provides a set of components that can then be used in place of a hook.
import { Formik } from "formik";const FormComponent = () => { return ( <Formik initialValues={{ name: "", email: "", }} onSubmit={(values) => { console.log(values); }} > {({ values, handleChange, handleSubmit }) => ( <form onSubmit={handleSubmit}> <div> <label> Name <input type="text" name="name" onChange={handleChange} value={values.name} /> </label> </div> <div> <label> Email <input type="text" name="email" onChange={handleChange} value={values.favoriteFood} /> </label> </div> <button type="submit">Submit</button> </form> )} </Formik> );};
This component isn't just useful as an alternative API, however. It also enabled us to use functionality like Formik's built-in Form and Field components, which allows us to remove the onSubmit and onChange method passing for a more terse API.
import { Formik, Form, Field } from "formik";const FormComponent = () => { return ( <Formik initialValues={{ name: '', email: '', }} onSubmit={(values) => { console.log(values); }} > {() => ( <Form> <div> <label> Name <Field type="text" name="name" /> </label> </div> <div> <label> Email <Field type="text" name="email" /> </label> </div> <button type="submit">Submit</button> </Form> )} </Formik> );};Formik Component - StackBlitz
Editimport { Formik, Form, Field } from "formik";const FormComponent = () => { return ( <Formik initialValues={{ name: "", email: "", }} onSubmit={(values) => { console.log(values); }} > {() => ( <Form> <div> <label> Name <Field type="text" name="name" /> </label> </div> <div> <label> Email <Field type="text" name="email" /> </label> </div> <button type="submit">Submit</button> </Form> )} </Formik> );};export default FormComponent;Keep in mind that the
FieldandFormcomponents will not work when usinguseFormik. This is because of underlying implementation details that rely on React's Dependency Injection. Instead, you'd have to passonChangeandonSubmit, respectively, toinputandformHTML elements, as we demonstrated before.
Input States
Formik provides the states for:
- Touched fields: Fields that have been entered into by the user, but may not yet have typed anything into
- Dirty forms: If any fields have had input typed into them
- Submitted forms
const FormComponent = () => { const [isPending, setIsPending] = useState(false); return ( <Formik initialValues={{ name: '', email: '', }} onSubmit={(values) => { setIsPending(true); sendToServer(values).then(() => setIsPending(false)); }} > {({ touched, dirty, isSubmitting }) => ( <Form> <div> <label> Name <Field type="text" name="name" /> </label> {touched.name && <p>This field has been touched</p>} {!touched.name && <p>This field has not been touched</p>} </div> <div> <label> Disabled Field <Field type="text" name="email" disabled /> </label> </div> {/* Formik doesn't provide "dirty" on a field-level basis */} {dirty && <p>This form is dirty</p>} {isSubmitting && <p>Form is submitted</p>} {isPending && <p>Form is pending</p>} <button type="submit">Submit</button> </Form> )} </Formik> );};// Pretend this is calling to a serverfunction sendToServer(formData) { // Wait 4 seconds, then resolve promise return new Promise((resolve) => setTimeout(() => resolve(), 4000));}Input States - StackBlitz
Editimport { useState } from "react";import { Formik, Form, Field } from "formik";const FormComponent = () => { const [isPending, setIsPending] = useState(false); return ( <Formik initialValues={{ name: "", email: "", }} onSubmit={(values) => { setIsPending(true); sendToServer(values).then(() => setIsPending(false)); }} > {({ touched, dirty, isSubmitting }) => ( <Form> <div> <label> Name <Field type="text" name="name" /> </label> {touched.name && <p>This field has been touched</p>} {!touched.name && <p>This field has not been touched</p>} </div> <div> <label> Disabled Field <Field type="text" name="email" disabled /> </label> </div> {/* Formik doesn't provide "dirty" on a field-level basis */} {dirty && <p>This form is dirty</p>} {isSubmitting && <p>Form is submitted</p>} {isPending && <p>Form is pending</p>} <button type="submit">Submit</button> </Form> )} </Formik> );};// Pretend this is calling to a serverfunction sendToServer(formData) { // Wait 4 seconds, then resolve promise return new Promise((resolve) => setTimeout(() => resolve(), 4000));}export default FormComponent;Form Arrays
Formik provides a FieldArray to help make handling arrays easier with Formik fields. Similar to Formik's Field and Form components, FieldArray only works when using the Formik component instead of the useFormik hook.
import { Formik, Form, Field, FieldArray } from "formik";// We'll explain why we need an id a bit laterlet id = 0;export const FriendList = () => ( <div> <h1>Friend List</h1> <Formik initialValues={{ users: [{name: "", id: ++id}] }} onSubmit={(values) => console.log(values)} > {({ values }) => ( <Form> <FieldArray name="users" render={(arrayHelpers) => ( <div> {values.users.map((user, index) => ( <div key={index}> <label> Name <Field name={`users.${index}.name`} /> </label> <button type="button" onClick={() => arrayHelpers.remove(index)} // remove a friend from the list > Remove User </button> </div> ))} <button type="button" onClick={() => arrayHelpers.push({ name: "", id: ++id })} > Add user </button> <button type="submit">Submit</button> </div> )} /> </Form> )} </Formik> </div>);
You may notice that we're using
arrayHelpers.pushandarrayHelpers.removeinstead of simply doingvalues.push. This is because if we do avalues.pushcommand, it won't trigger a re-render.
Because we're now using an array, we need a unique ID for each user. This is why, for each implementation, there's an id field. We then use this id field to identify which user is which to the framework.
Form Arrays - StackBlitz
Editimport { Formik, Form, Field, FieldArray } from "formik";// We'll explain why we need an id a bit laterlet id = 0;export const FriendList = () => ( <div> <h1>Friend List</h1> <Formik initialValues={{ users: [{ name: "", id: ++id }] }} onSubmit={(values) => console.log(values)} > {({ values }) => ( <Form> <FieldArray name="users" render={(arrayHelpers) => ( <div> {values.users.map((user, index) => ( <div key={index}> <label> Name <Field name={`users.${index}.name`} /> </label> <button type="button" onClick={() => arrayHelpers.remove(index)} // remove a friend from the list > Remove User </button> </div> ))} <button type="button" onClick={() => arrayHelpers.push({ name: "", id: ++id })} > Add user </button> <button type="submit">Submit</button> </div> )} /> </Form> )} </Formik> </div>);export default FriendList;Form Validation
Formik allows you to pass a function to the Field component in order to validate the data input. Here, we can simply check if value is present or not. Then, we can check against the Formik component's errors field to see if the name field has any errors.
function requiredField(value) { let error; if (!value) { error = 'Required'; } return error;}const FormComp = () => { return ( <Formik initialValues={{ name: '' }} onSubmit={(val) => console.log(val)}> {({ errors, touched }) => ( <Form> <div> <Field name="name" validate={requiredField} /> </div> {errors.name && touched && <div>{errors.name}</div>} <button type="submit">Submit</button> </Form> )} </Formik> );};Form Validation - StackBlitz
Editimport { Formik, Form, Field } from "formik";function requiredField(value) { let error; if (!value) { error = "Required"; } return error;}const FormComp = () => { return ( <Formik initialValues={{ name: "" }} onSubmit={(val) => console.log(val)}> {({ errors, touched }) => ( <Form> <div> <Field name="name" validate={requiredField} /> </div> {errors.name && touched && <div>{errors.name}</div>} <button type="submit">Submit</button> </Form> )} </Formik> );};export default FormComp;Complex Data Schema
Formik's validate function passing works quite well for basic usage. That said, let's introduce a better way to do form validation that scales a little better when dealing with more complex data.
There are multiple different libraries that will integrate with Formik to add dedicated complex validation functionality. yup is one such library.
By using yup, you're able to replace our home-grown function with something as simple as yup.string().required() to mark the field as required.
Yup works by introducing the concept of a "schema" into form validation. You start by declaring a schema that's then validated against using the validationSchema in the Formik component.
import { Formik, Form, Field } from 'formik';import * as yup from 'yup';const FormSchema = yup.object().shape({ name: yup.string().required(),});const FormComponent = () => { return ( <Formik initialValues={{ name: '', }} validationSchema={FormSchema} onSubmit={(values) => { console.log(values); }} > {({ errors }) => ( <Form> <div> <label> Name <Field type="text" name="name" /> </label> {errors.name && <p>{errors.name}</p>} </div> <button type="submit">Submit</button> </Form> )} </Formik> );};Complex Data Schema - StackBlitz
Editimport { Formik, Form, Field } from "formik";import * as yup from "yup";const FormSchema = yup.object().shape({ name: yup.string().required(),});const FormComponent = () => { return ( <Formik initialValues={{ name: "", }} validationSchema={FormSchema} onSubmit={(values) => { console.log(values); }} > {({ errors }) => ( <Form> <div> <label> Name <Field type="text" name="name" /> </label> {errors.name && <p>{errors.name}</p>} </div> <button type="submit">Submit</button> </Form> )} </Formik> );};export default FormComponent;While Yup will generate an error message based on the expected and received data types, we're also able to customize the error message ourselves:
const FormSchema = yup.object().shape({ name: yup.string().required("You must input a name of the user to share with."),});
Validation Types
Marking a field as required is far from the only type of form validation. While there are any number of items, there
- Minimum string length
- Maximum string length
- Two inputs match each other
- Match a regex
Here's a playground where we demonstrate a form that validates all of those examples:
import { Formik, Form, Field } from 'formik';import * as yup from 'yup';const FormSchema = yup.object().shape({ minLenStr: yup.string().min(3), maxLenStr: yup.string().max(3), regex: yup.string().matches(/hello|hi/i), pass: yup.string(), confirm: yup .string() .oneOf([yup.ref('pass'), null], 'Must match "password" field value'),});const FormComponent = () => { return ( <Formik initialValues={{ name: '', }} validationSchema={FormSchema} onSubmit={(values) => { console.log(values); }} > {({ errors }) => ( <Form> <div> <label> Minimum Length String (3) <Field type="text" name="minLenStr" /> </label> {errors.minLenStr && <p>{errors.minLenStr}</p>} </div> <div> <label> Maximum Length String (3) <Field type="text" name="maxLenStr" /> </label> {errors.maxLenStr && <p>{errors.maxLenStr}</p>} </div> <div> <label> Regex <Field type="text" name="regex" /> </label> {errors.regex && <p>{errors.regex}</p>} </div> <div> <label> Password <Field type="password" name="pass" /> </label> </div> <div> <label> Password Confirm <Field type="password" name="confirm" /> </label> {errors.confirm && <p>{errors.confirm}</p>} </div> <button type="submit">Submit</button> </Form> )} </Formik> );};Validation Types - StackBlitz
Editimport { Formik, Form, Field } from "formik";import * as yup from "yup";const FormSchema = yup.object().shape({ minLenStr: yup.string().min(3), maxLenStr: yup.string().max(3), regex: yup.string().matches(/hello|hi/i), pass: yup.string(), confirm: yup .string() .oneOf([yup.ref("pass"), null], 'Must match "password" field value'),});const FormComponent = () => { return ( <Formik initialValues={{ name: "", }} validationSchema={FormSchema} onSubmit={(values) => { console.log(values); }} > {({ errors }) => ( <Form> <div> <label> Minimum Length String (3) <Field type="text" name="minLenStr" /> </label> {errors.minLenStr && <p>{errors.minLenStr}</p>} </div> <div> <label> Maximum Length String (3) <Field type="text" name="maxLenStr" /> </label> {errors.maxLenStr && <p>{errors.maxLenStr}</p>} </div> <div> <label> Regex <Field type="text" name="regex" /> </label> {errors.regex && <p>{errors.regex}</p>} </div> <div> <label> Password <Field type="password" name="pass" /> </label> </div> <div> <label> Password Confirm <Field type="password" name="confirm" /> </label> {errors.confirm && <p>{errors.confirm}</p>} </div> <button type="submit">Submit</button> </Form> )} </Formik> );};export default FormComponent;Non-Text Form Fields
Some form fields, like checkboxes, may not reflect their UI in a text-based form.
Luckily, Formik allows you to easily cast a Field component to a different type to display a different base input UI. This is the same as how the input element works.
const FormSchema = yup.object().shape({ termsAndConditions: yup .bool() .oneOf([true], 'You need to accept the terms and conditions'),});const FormComponent = () => { return ( <Formik initialValues={{ termsAndConditions: false, }} validationSchema={FormSchema} onSubmit={(values) => { console.log(values); }} > {({ errors }) => ( <Form> <div> <label> Terms and conditions <Field type="checkbox" name="termsAndConditions" /> </label> {errors.termsAndConditions && <p>{errors.termsAndConditions}</p>} </div> <button type="submit">Submit</button> </Form> )} </Formik> );};
Something worth mentioning in terms of validation is how Formik integrates with Yup; we can't simply mark our
termsAndConditionsfield asrequired. Instead, we have to tellyupthat it has to beoneOf([true])to enforce the checkbox to betrue.
Non-Text Form Fields - StackBlitz
Editimport { Formik, Form, Field } from "formik";import * as yup from "yup";const FormSchema = yup.object().shape({ termsAndConditions: yup .bool() .oneOf([true], "You need to accept the terms and conditions"),});const FormComponent = () => { return ( <Formik initialValues={{ termsAndConditions: false, }} validationSchema={FormSchema} onSubmit={(values) => { console.log(values); }} > {({ errors }) => ( <Form> <div> <label> Terms and conditions <Field type="checkbox" name="termsAndConditions" /> </label> {errors.termsAndConditions && <p>{errors.termsAndConditions}</p>} </div> <button type="submit">Submit</button> </Form> )} </Formik> );};export default FormComponent;Conclusion
Formik is a powerful library that simplifies form handling in React. From basic form setup to complex validation and array handling, Formik provides a comprehensive set of tools to manage form state, validation, and submission. Whether you prefer using hooks or components, Formik has you covered.
If you enjoyed this article, you might also like my book series "The Framework Field Guide" that teaches React, Angular, and Vue for free.
Happy coding!