Formik is a popular tool that helps us write React forms and keep their code consistent across even big projects. Besides having a fixed number of properties, we sometimes need to allow the user to shape the form to some extent and create recursive data structures. In this article, we learn how to do that with Formik and build the following form:
Defining the basics of our form
Let’s start with defining our data structure. The crucial thing about our form is that it is recursive.
If you want to know more about recursion, check out Using recursion to traverse data structures. Execution context and the call stack
DynamicFormProperty.tsx
1 2 3 4 5 6 7 |
interface DynamicFormProperty { id: string; label: string; properties: DynamicFormProperty[]; } export default DynamicFormProperty; |
We add the id property above to use with the key prop.
First, we need to create the entry point that uses the <Formik /> component to initialize our form.
DynamicForm.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
import React from 'react'; import { Formik, Form } from 'formik'; import useDynamicForm from './useDynamicForm'; import FormProperty from './FormProperty/FormProperty'; import styles from './DynamicForm.module.scss'; const DynamicForm = () => { const { handleSubmit, initialValues } = useDynamicForm(); return ( <Formik onSubmit={handleSubmit} initialValues={initialValues}> <Form className={styles.form}> <FormProperty /> <button className={styles.submitButton} type="submit"> Submit </button> </Form> </Formik> ); }; export default DynamicForm; |
Above, I’m using SCSS modules to style this simple component.
We initiate the form with simple values for this demonstration and log them when the user clicks on the submit button.
useDynamicForm.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
import DynamicFormProperty from './DynamicFormProperty'; function useDynamicForm() { const initialValues: DynamicFormProperty = { label: '', properties: [], }; const handleSubmit = (values: DynamicFormProperty) => { console.log(values); }; return { initialValues, handleSubmit, }; } export default useDynamicForm; |
Creating an elementary text input
In this article, we create a form that has text inputs. Because of that, we need to create a component to handle them.
TextInput.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
import React, { FunctionComponent } from 'react'; import { useField } from 'formik'; import styles from './TextInput.module.scss'; interface Props { name: string; } const TextInput: FunctionComponent<Props> = ({ name }) => { const [field] = useField({ name }); return <input {...field} className={styles.input} />; }; export default TextInput; |
Above, we use the useField function to interact with Formik. If you want to know more, go to Building forms using Formik with the React Hooks API and TypeScript
Our TextInput is very simple to use. We need to provide it with the path to a form field we want it to use.
1 |
<TextInput name="label" /> |
1 |
<TextInput name="properties[0].label" /> |
Accessing nested properties of the form
To access a nested property of an object, we can use the getIn function provided by Formik. It returns a value based on the provided path.
The getIn function works very similarly to the get function provided by Lodash.
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 |
import { getIn } from 'formik'; import DynamicFormProperty from '../DynamicFormProperty'; const values: DynamicFormProperty = { id: 'root', label: 'root', properties: [ { label: 'second-level-1', id: '2aa571ca-0b65-43ad-8309-24786f07c795', properties: [], }, { label: 'second-level-2', id: '9717c75f-1421-4518-a1fc-b404d1da001c', properties: [ { label: 'third-level-1', id: 'f1645fc1-1dac-4fa9-8b8a-d4427fbb9fec', properties: [], }, ], }, ], }; const thirdLevelLabel = getIn(values, 'properties[1].properties[0].label'); console.log(thirdLevelLabel); // third-level-1 |
Since we need a full path to access a particular property, we have to keep track of how deep we are in our form. Because of that, our FormProperty has the prefix property.
FormProperty.tsx
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 |
import React, { FunctionComponent } from 'react'; import TextInput from '../TextInput/TextInput'; import useFormProperty from './useFormProperty'; import styles from './FormProperty.module.scss'; interface Props { prefix?: string; } const FormProperty: FunctionComponent<Props> = ({ prefix = '' }) => { const { properties, addNewProperty, removeProperty } = useFormProperty(prefix); return ( <div className={styles.wrapper}> <div className={styles.labelContainer}> <TextInput name={`${prefix}label`} /> <button type="button" onClick={addNewProperty} className={styles.addPropertyButton} > + </button> </div> {properties.map((property, index) => ( <div className={styles.propertyContainer}> {property.id !== 'root' && ( <button type="button" onClick={removeProperty(index)} className={styles.removePropertyButton} > - </button> )} <FormProperty key={property.id} prefix={`${prefix}properties[${index}].`} /> </div> ))} </div> ); }; export default FormProperty; |
Some of the possible values for the prefix prop that the FormProperty uses are the following:
- an empty string
- properties[0].
- properties[1].
- properties[1].properties[0].
- properties[1].properties[0].properties[0].
- …
To access the array of properties, we need to get the form values first. To do that, we can use the useFormikContext hook.
1 |
const { values } = useFormikContext(); |
Paired with the getIn, we can use the above to access a particular array of properties.
1 2 3 4 |
const properties: DynamicFormProperty[] = getIn( values, `${prefix}properties`, ); |
The crucial thing to notice above is that we use the index value when mapping the array of properties. Thanks to that, we can set the correct prefix.
1 2 3 4 |
<FormProperty key={property.id} prefix={`${prefix}properties[${index}].`} /> |
Modifying the array of properties
To add or remove properties, we need to modify the right properties array. To do that, we can use the setFieldValue function provided by the useFormikContext hook. It uses the path of the property in the same way the getIn function does.
Adding properties
We can use the uuid library to generate unique ids when adding a new property. To do that, we can install it with npm.
1 |
npm install uuid @types/uuid |
useFormProperty.tsx
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 |
import { useFormikContext, getIn } from 'formik'; import DynamicFormProperty from '../DynamicFormProperty'; import { v4 as uuid } from 'uuid'; function useFormProperty(prefix: string) { const { values, setFieldValue } = useFormikContext(); const properties: DynamicFormProperty[] = getIn( values, `${prefix}properties`, ); const addNewProperty = () => { const newProperty: DynamicFormProperty = { label: '', id: uuid(), properties: [], }; const newProperties = [...properties, newProperty]; setFieldValue(`${prefix}properties`, newProperties); }; // ... return { properties, addNewProperty, removeProperty, }; } export default useFormProperty; |
Removing properties
We can delete an element from the array using a given index to remove a property. When put together, our hook likes like that:
useFormProperty.tsx
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 |
import { useFormikContext, getIn } from 'formik'; import DynamicFormProperty from '../DynamicFormProperty'; import { v4 as uuid } from 'uuid'; function useFormProperty(prefix: string) { const { values, setFieldValue } = useFormikContext(); const properties: DynamicFormProperty[] = getIn( values, `${prefix}properties`, ); const addNewProperty = () => { const newProperty: DynamicFormProperty = { label: '', id: uuid(), properties: [], }; const newProperties = [...properties, newProperty]; setFieldValue(`${prefix}properties`, newProperties); }; const removeProperty = (propertyIndex: number) => () => { const newProperties = [...properties]; newProperties.splice(propertyIndex, 1); setFieldValue(`${prefix}properties`, newProperties); }; return { properties, addNewProperty, removeProperty, }; } export default useFormProperty; |
Summary
In this article, we’ve gone through the idea of creating dynamic, recursive forms with Formik and TypeScript. When doing that, we had to use various hooks provided by Formik to access and modify the data directly. We’ve also used the getIn that Formik exposes, but it is not thoroughly documented. That knowledge helped us create a fully functional flow that allows the user to add and remove fields.