Skip to main content

Manage React form state with Formik

Manage React form
(Image credit: Matt Crouch)

Welcome to our guide on how to manage React form state with Formik. Form elements hold their values in their own internal state – at odds with React's state-driven approach. To make the two work together, we need a more involved solution, such as using controlled components for each field.

But there are many more aspects to creating a form than just capturing data. We need to check the formatting is correct, all required fields have been set and that everything was sent successfully when submitted. That leads to a lot of complicated logic that can build up fast. This is where Formik can help. 

For more resources to help improve your web design, check out our rundown of web design tools and cloud storage options. Starting from scratch? Choose the right website builder and get your web hosting service right with our roundups.

What is Formik?

Formik is a lightweight library that handles these problems. By wrapping a form with the supplied components, we get a lot of this behaviour for free. All we need to supply is the code to handle what makes our form unique.

To help us explore what Formik can do, we will be building a small voting application. The user can enter their name and vote on one of the supplied options. If everything is fine, the form will submit but, if not, the user receives an error message.

01. Get started

To start, open the 'tutorial' directory from the tutorial files on the command line. Install the dependencies and start the server. This app is built upon create-react-app and includes its packages, along with Formik itself and a couple of others to help with styling.

> npm install
> npm start

The development server will start and the application then opens in the browser. The App.js file controls the whole application that will render our container component for the form. Open up src/App.js and import the <VoteContainer> component.

import VoteContainer from “./VoteContainer”;

Then add it into the application.

<section className=”vote-container”>
  <VoteContainer />
</section>

02. Create Formik form

Formik forms can be created in two ways. The withFormik higher-order component enables us to wrap an existing component or use the <Formik> component with a render prop that performs the same function, which is what we will be using.

Inside VoteContainer.js, create a functional component that will hold all the logic for the form. This returns a <Formik> component that renders our form. Provide a starting value for the fields we will add later on through the initialValues prop.

import { Formik } from “formik”;
import Vote from “./Vote”;

function VoteContainer() {
  return <Formik
    initialValues={{ name: “”, answer: “” }}
render={props => <Vote {...props} />} />

03. Create Vote component

The <Vote> component holds the form structure. By having the Formik logic separate we can keep the form component as simple as possible.

Create a Vote component inside Vote.js that makes use of the <Form> component from Formik. Add a button to submit the form as normal.

import { Form } from “formik”;
function Vote() {
  return (
    <Form className=”vote”>
      <input type=”submit” value=”Vote now” />
    </Form>
  );
}

04. Add fields

Formik keeps track of the changes to each field and will provide them once the form is submitted. It does all this through the events emitted from the form and each field within it.

At a form level there are two specific events – submit and reset. When a form submits, we need Formik to take over and perform its checks, with the reset event clearing the state. The imported <Form> component binds these events to Formik.

We can now start adding our first field. Each vote needs to be accompanied with a name, which means we need a text input first.

The <Field> component does the same job as <Form> does for the whole form. It binds the necessary events and props such as a name and value in order to display field state.

Add a field into the form and connect it to a label. These work as they would in regular HTML forms.

import { Field } from “formik”;
<label htmlFor=”name”>Name</label>
<Field autoComplete=”name” id=”name” name=”name” type=”text” />

05. Supply logic

We don't need to work with any browser events to submit, as the onSubmit event is handled for us. But we do need to supply the logic to handle submission. The callback is still called onSubmit but it instead receives the form values directly. It also receives the 'bag' – an object containing a few methods to interact with the form while it submits.

As this data would typically head to a server, this function can also be asynchronous. Formik has a special isSubmitting prop that it sets to true automatically once the submission starts. With an async function, we can wait until the form has submitted and set that back to false. 

Back inside VoteContainer.js, we can add our submission logic. Create the function and pass it to the <Formik> component. For this example, we won’t be sending to a server but we can use a delayed Promise to simulate the network latency.

const onSubmit = async (values, bag) => {
    await new Promise(resolve => setTimeout(resolve, 1000));
    bag.setSubmitting(false);
    console.log(“Form submitted”, values);
  };
return <Formik [...] onSubmit={onSubmit} />;

06. Display state

We also need to display that submitting state within the form. To prevent a double submission, we can disable the button while the form is submitting. Formik passes this into the form inside Vote.js as a prop. We can pull this out and apply it to the button.

function Vote({ isSubmitting }) {[…]}
<input disabled={isSubmitting} type=”submit” value=”Vote now” />

07. Add name field

At the moment the form can be submitted without a name being entered. As this is a required field, we should flag this to the user.

The root <Formik> component also takes a validate prop, which is a function that performs validation and returns an object. Each key-value pair represents a field and an error message. If a field has no value in this object, it is deemed to be valid. The form will only submit when this function returns an empty object. The function receives the form values as an argument. For a required field, we only need to check the value is not an empty string.

Back inside VoteContainer.js, create a validate callback function to check this value and hook it up to Formik.

const validate = values => {
    const errors = {};
    if (values.name === “”) {
      errors.name = “Name is required”;
    }
    return errors;
  };
return <Formik [...] validate={validate} />;

08. Display errors

These errors are then passed to Vote.js as an errors prop. To display errors inline, we need to match up the errors to the particular form field.

function Vote({ isSubmitting, errors, touched }) {[…]}
<div className=”input-group”>
  <label htmlFor=”name”>Name</label>
  <div
    className={classNames({
      “validation-group”: true,
      error: !!errors.name && touched.name
    })}
  >
    <Field autoComplete=”name” id=”name” name=”name” type=”text” />
    {!!errors.name && touched.name && (
      <div className=”error-message”>{errors.name}</div>
    )}
  </div>
</div>

Formik will validate the form every time it updates. A form with lots of fields would immediately be swamped with errors after the first change. To avoid this, we only display the error when a field has been 'touched', meaning it has been interacted with at some point. When a form submits, Formik will touch all fields and show any hidden errors.

09. Add answer field

(Image: © Matt Crouch)

With the name field complete, we can move to the answers. The approach we’ve used so far works well for regular text inputs but is not suited to multiple inputs that fall under the same field name, such as a group of radio buttons.

While we can include multiple <Field> components with the same name, we should avoid repeating ourselves as much as possible. Instead, Formik lets us pass a custom component into <Field> that can manage them as one.

The <AnswerGroup> component works similar to the text input. It takes an options prop, which is an array containing the options to display. These are transformed into styled radio buttons that let users choose an answer. Include this component within Vote.js. By using <Field> it gets passed the same props as the name input as well as any extras, in this case the OPTIONS constant. 

import AnswerGroup from “./AnswerGroup”;
<Field component={AnswerGroup} options={OPTIONS} name=”answer” />

(Image: © Matt Crouch)

Finally, we need to require an answer in our validation function in VoteContainer.js as well. The process there is the same as with the name field.

if (values.answer === “”) {
  errors.answer = “Answer is required”;

By keeping the validation and submission logic separate and using Formik to stitch everything together, we are able to keep each piece small and easy to understand. 

This content originally appeared in net magazine. Read more of our web design articles here

Read more: