Formik Form Handling

Last updated: 2023-08-19 (2 years ago)
Formik is an open-source React library meant to simplify form handling in a application. It provides a solid foundation by partly taking care of tracking values, raising errors, running validation checks and submitting final values.
The goal of this article is to go through a couple of different scenarios using Formik, starting with a simple one and building up difficulty as we go.

Simple Form

Down to its simplest, a form is an interface to retrieve values from the user, and forwarding these to another page or server for further processing. Please find below a simple example of a Formik form:
Username
Favourite game
Form values submitted:
That's as easy as it gets, two text fields, no validation whatsoever, and we are just printing the submitted values. Now let's take a look at the code used to render that form. As always, please note that the css has been removed to keep the relevant part clearer.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import { Field, Form, Formik } from 'formik'; export const SimpleForm = ({ onSubmit }: any) => { const initialValues = { username: "", favFood: "", } return ( <Formik initialValues={initialValues} onSubmit={onSubmit}> <Form> <div> <p>Username</p> <Field id="username" name="username" /> <br /> <p>Favourite food</p> <Field id="favFood" name="favFood" /> </div> <button type="submit">Submit</button> </Form> </Formik> ); };
Even though this example is basic, there is still a couple of things worthy of note:
  • A 'Formik' component sits at the top, and is expecting to be given an object of initial values. By default, each key will be used internally used by Formik to maintain the form values, and to know what value changed.
  • 'Formik' component also receives as props a callback method to be executed upon submitting the form. This function will get as parameter an object holding all the latest values of the form.
  • Below that is a 'Form' component, wrapping the actual inputs we are providing the user with.
  • For each input, we are leveraging the 'Field' component imported from formik library. Here you have to give a 'name' attribute to the Field, that matches the keys from the initial values passed to Formik, otherwise formik won't be able to tell which field should update which value.
Let's build on top of that and add validation to our form, which is an extremely common requirement, whether to prevent certain values, or make a field required.

Intermediate form with validation

This next form adds one more field, and more importantly brings validation into the mix.
Username
Favourite game
Score
Form values submitted:
We are going to use a validation library called Yup to help us streamline the process of checking if our fields were correctly filled out, and enforcing rules we wish the user to follow. The good news: formik supports by default Yup schemas. Please find below the code (again stripped off the css):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 import { Field, Form, Formik } from 'formik'; import * as Yup from 'yup'; export const FormWithValidation = ({ onSubmit }: any) => { /* This object is defining how we want to restrict each field, and the associated error message if a rule is not respected */ const validationSchemaYup = Yup.object().shape({ username: Yup.string() .required('Username is mandatory'), favGame: Yup.string() .not(["E.T. on Atari", "Dragon's lair on NES"], "This game isn't very good, I won't have it!") .required('Favourite game is mandatory'), score: Yup.number() .min(0, 'Score can not be negative') .max(100, 'Score can not exceed a hundred') .required('Score is mandatory'), }); const initialValues = { username: "", favGame: "", score: 50, } return ( <Formik initialValues={initialValues} validationSchema={validationSchemaYup} onSubmit={onSubmit}> {({ errors, touched }) => ( <Form> <div> <div> <p>Username</p> <Field id="username" name="username" /> {errors.username && touched.username ? ( <div>{errors.username}</div> ) : null} </div> <div> <p>Favourite game</p> <Field id="favGame" name="favGame" /> {errors.favGame && touched.favGame ? ( <div>{errors.favGame}</div> ) : null} </div> <div> <p>Score</p> <Field id="score" name="score" /> {errors.score && touched.score ? ( <div>{errors.score}</div> ) : null} </div> </div> <button type="submit">Submit</button> </Form> )} </Formik> ); };
Now a couple of comments regarding what we've just added to the basic form:
  • Line 7, the main change is the addition of the validation schema through Yup. The 'username' field's only restriction is not to be undefined. 'favGame' is also a required field, but we are preventing 2 games (arbitrarily, sorry if you like these 2!), and the 'score' is required and needs to be within the 0-100 range;
  • Line 28, we are setting the validation schema to the Formik form, so it will know the rules we wish to enforce;
  • Line 30, is an important change, as we are now tapping into Formik state variable that are made available to help us. Here we are accessing 2 of them, 'errors' is an object containing what field did not succeed the validation, and 'touched' an object containing which fields were visited by the user;
  • Block line 36, is how we chose to conditionally display the error message. We are showing the error message only if the field's validation has failed (obviously), but also only if the field has been visited already. Indeed, it is common practice not to show errors when the form has just appeared on the screen, before the user has even had time to interact with it;
  • Go ahead and try tinkering with the form, filling negative values to the score, or not filling out the username, each field has its own custom error message as defined in the Yup schema. As long as an error exists within the form, the submit button will not have any effect. Note that we do not need to disable the button ourselves, Formik knows not to submit the form all by itself, because it is enforcing the validation schema. Of course if we want to gray out the button we can do it, but it would work anyway.
So far we've let Formik take care of keeping track of each value for us, but there might be cases where our inputs get a little too complex for Formik to handle itself. That's the focus of the last section.

Formik with custom field setters

This final form is asking the user for a time range, a high score and a number of games played. Enhancements from the previous form include:
  • Validation of an field whose type is an object (here 'Time played');
  • Custom validation function to make one field dependent on another one. To illustrate that example, we've chosen the following rule: a high score of 1000 or above is possible only if you've played at least 10 games;
  • We will be setting the fields' value ourselves, instead on relying on the 'Field' component made available by Formik. This method is good to know if you want to have fine control over the way values are being set and touched within the form;
  • On top of that we have decided to extract the submit button out of the Formik form, as this use case may happen in your application.
Username
Time played
High score
Number of games played
Form values submitted:
And without further ado, the code (broken into two parts because it has become quite sizeable):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 import { Form, Formik } from 'formik'; import * as Yup from 'yup'; export const FormWithCustomSetter = ({ onSubmit, formRef }) => { // Custom Yup validator to add our own custom rule to the high score field. Yup.addMethod(Yup.number, 'validateHighScore', function (errorMessage) { return this.test('validate-high-score', errorMessage, function (value) { if (!value) return; // path: ID of the field being updated. // createError: utility function to let Formik know the field is invalid. // parent: to access a list of all Formik values. const { path, createError, parent } = this; // Throws a custom error if the number of games played is less than 10, while high score > 1000. if (value < 10 && parent.highScore > 1000) { return createError({ path, message: 'Impossible to have a high score above 1000 with that few games played!' }); } // Returning true means there is no error return true; }); }); const validationSchemaYup = Yup.object().shape({ username: Yup.string() .required('Username is mandatory'), timePlayed: Yup.object().shape({ startTime: Yup.string().required(), endTime: Yup.string().required(), }), highScore: Yup.number() .min(0, 'Best score must be positive integer') .required('Best score is mandatory'), nbGamesPlayed: Yup.number() .min(0, 'Number of games played must be a positive integer') .validateHighScore() .required('Number of games played is mandatory'), }); const initialValues = { username: "", timePlayed: null, highScore: "", nbGamesPlayed: "", } return ( /** Omitted for clarity, please see next snippet for the render **/ ); };
This first snippet is showing our validation process:
  • Block line 6, we are adding our own method to Yup, by calling 'Yup.addMethod', which takes 3 parameters. In this instance, we are adding it to the 'Yup.number' schema, since we want to apply that validator to a number field, so the first parameter is that schema. The second parameter is the name we want to give our new validation method, and the third is the actual validation logic;
  • Line 12, a comment is provided already, we are extracting three parameters from 'this' which was passed by Yup internally. 'path' is the id of the field being touched, useful because our validator can be reused for many fields. 'createError' is a method to create a Yup error, and 'parent' allows us to access the rest of the values being maintained by the form;
  • Line 15 and 16, we are checking if the custom rule we've defined has been broken (impossible to have a score higher than 1000 if you've played less than 10 games), if so, we throw an error;
  • Line 26, part of the validation schema, we now have 'timePlayed' field that we chose to keep track of through an object with two keys, startTime and endTime, both required string;
  • Line 33, the number of games played is the number we wish to apply our custom rule to, so we add the 'validateHighScore' method that we've previously added to Yup. It will be checked along with the other rules;
With the validation logic out of the way, let's have a peek at the implementation of our form:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 export const FormWithCustomSetter = ({ onSubmit, formRef }) => { /* Validation logic omitted, see previous snippet */ return ( <Formik initialValues={initialValues} validationSchema={validationSchemaYup} onSubmit={bodyForm => { const { startTime, endTime } = bodyForm.timePlayed; if (startTime.isValid() && endTime.isValid()) { onSubmit({ username: bodyForm.username, startTime: startTime.format('HH:mm:ss A'), endTime: endTime.format('HH:mm:ss A') , highScore: bodyForm.highScore, nbGamesPlayed: bodyForm.nbGamesPlayed }) } }} innerRef={formRef}> {({ values, errors, touched, setFieldValue, setFieldTouched }) => ( <Form> <div> <div> <p>Username</p> <input id="username" name="username" value={values.username} onChange={e => { setFieldTouched('username', true); setFieldValue('username', e.target.value); }}/> {errors.username && touched.username ? ( <div>{errors.username}</div> ) : null} </div> <div> <p>Time played</p> <TimePicker.RangePicker allowClear={false} onChange={newValue => { setFieldTouched('timePlayed', true); setFieldValue('timePlayed', { startTime: newValue?.[0], endTime: newValue?.[1], }); }} minuteStep={10} format='hh:mm A' value={values.timePlayed ? [values.timePlayed.startTime, values.timePlayed.endTime] : ['', '']}/> {errors.timePlayed && touched.timePlayed ? ( <div>{errors.timePlayed}</div> ) : null} </div> <div> <p>Score</p> <input id="highScore" name="highScore" value={values.highScore} onChange={e => { setFieldTouched('highScore', true); setFieldValue('highScore', e.target.value); }}/> {errors.highScore && touched.highScore ? ( <div>{errors.highScore}</div> ) : null} </div> <div> <p>Number of games played</p> <input id="nbGamesPlayed" name="nbGamesPlayed" value={values.nbGamesPlayed} onChange={e => { setFieldTouched('nbGamesPlayed', true); setFieldValue('nbGamesPlayed', e.target.value); }}/> {errors.nbGamesPlayed && touched.nbGamesPlayed ? ( <div>{errors.nbGamesPlayed}</div> ) : null} </div> </div> </Form> )} </Formik> ); };
This second snippet is focusing on the form's implementation. For information, the time range picker component is coming from antd library, and is internally using DaysJS to handle dates:
  • Line 5 and 6, same as with the previous forms, we are passing the initial values and validation schema;
  • Line 7, the 'onSubmit' props is a little more complex, as we wish to split the 'timePlayed' field up into two fields, startTime and endTime, after making sure they are valid DaysJS objects;
  • Line 19, is necessary to be able to call the 'submit' button outside of the Formik component. For that, need to create a reference using the React hook useRef, which we will pass along to Formik, in order for us to access its methods. An example of how to set up the ref is provided later on in this article;
  • Line 20, we are accessing a couple of additional Formik state variables, namely setFieldValue and setFieldTouched that we will call ourselves when the values of our inputs change;
  • Block line 25, since we got rid of the 'Field' component from Formik, that means we need to explicitly pass an 'onChange' prop to the input. Once the user types anything in the box, it's going to set the field as 'touched', and set the new value;
  • Block line 39, is the range picker from antd, the logic is similar to our regular text inputs, but we do need to split the value given by the time picker component (which is an array of 2 DaysJS object), into the object we've set up to that purpose.
As a final snippet, let's see how we are supposed to set up the useRef() hook in order to trigger the form's submit from anywhere in our app:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 const YourParentComponent = () => { const formRef = useRef<HTMLFormElement>(null); const onSubmit = () => { if (formRef.current?.handleSubmit) { formRef.current.handleSubmit(); } } return <> <FormWithCustomSetter onSubmit={(formValues: object) => { console.log(formValues) }} formRef={formRef}/> <button onClick={onSubmit}> <p>External Submit</p> </button> </> }
We are initialising a reference that we pass to the Formik component, which is going to attach itself to it. After that, we can trigger the form into submitting the values by calling 'formRef.current.handleSubmit()' from wherever we see fit.
Thanks for reading, that's all for that article, hopefully these few examples will help you handle Formik forms effectively. Take care =)

References