React forms with Formik and Unit Testing with react-testing-library
Setup
Formik is a nice library to speed up the process of creating forms in React. It handles all the basic functionality like the form state, validation and submission.
Let’s begin by creating an empty create-react-app https://create-react-app.dev/
npx create-react-app my-app
Install the formik library
yarn add formik
Using formik components
Remove the example code generated and import the formik library and some components in App.js. We will use the react
useState
hook just to show the values of the form in this example.
import React, { useState } from 'react'
import { Formik, Form, Field } from 'formik'function App() {
const [result, setResult] = useState('') return (
// Formik is the main component that handles all the logic
// Form is just a regular html <form> wrapper
<Formik>
{() => (
<Form>
</Form>
)}
</Formik>
);
}export default App
Let’s use the
Field
formik component to create some fields. Check the docs to see all possible props available https://jaredpalmer.com/formik/docs/api/field#props-1
A default Field is an input type=”text”
<Field name="fieldName" />
For the select, we specify the options as children
<Field as="select" name="color" value="none">
<option value="none">Pick a color</option>
<option value="red">Red</option>
<option value="green">Green</option>
<option value="blue">Blue</option>
</Field>
In this example we are going to use a textarea to show the values after submit
<Field as="textarea" value={result} rows={5} />
Form props are passed down to children giving many options for custom fields
<Field name="email">
{({ field }) => (
<div>
<label>email:</label>
<input type="email" required placeholder="Email" {...field} />
</div>
)}
</Field>
We can go further and just set a component prop. Form props will passed to this component
function CustomInput({ field, form, ...props }) {
return <input {...field} {...props} />
}
<Field
name="name"
required
placeholder="Name"
component={CustomInput}
/>
A
type="submit"
will automatically handle form onSubmit
<button type="submit">Submit</button>
Put everything together
This is the final example form created with formik. Formik needs the
initialValues
prop to work properly.
import React, { useState } from 'react'
import { Formik, Form, Field } from 'formik'function CustomInput({ field, form, ...props }) {
return <input {...field} {...props} />
}function App() {
const [result, setResult] = useState('') return (
<Formik
initialValues={{
email: '',
name: '',
color: 'red'
}}
onSubmit={(values, actions) => {
setResult(JSON.stringify(values))
}}
>
{() => (
<Form>
<Field as="select" name="color" value="none">
<option value="none">Pick a color</option>
<option value="red">Red</option>
<option value="green">Green</option>
<option value="blue">Blue</option>
</Field>
<br />
<Field name="email">
{({ field }) => (
<div>
<label>email:</label>
<input
type="email"
required
placeholder="Email"
{...field}
/>
</div>
)}
</Field>
<br />
<Field
name="name"
required
placeholder="Name"
component={CustomInput}
/>
<br />
<button type="submit">Submit</button>
<br />
<br />
<Field
style={{ width: '100%'}}
as="textarea"
value={result}
rows={2}
/>
</Form>
)}
</Formik>
)
}export default App
Not so fancy, but I didn’t wanted to pollute the code with styling.
Unit testing with React Testing Library
Normally it’s enough to use Jest and Enzyme to test components, but with Formik, internal React state and events get more complex under the hood and a simple
input.simulate('change')
doesn’t work. Luckily there’s a nice piece of software called Testing Library that has support for many frontend libraries and frameworks https://testing-library.com/docs/intro
First of all, install the library as dev dependency
yarn add --dev '@testing-library/react'
Test if the App doesn’t crash first
it('renders without crashing', () => {
const div = document.createElement('div')
ReactDOM.render(<App />, div)
})
Now let’s pick all the field elements to be able to fire events with them
it('submits correct values', () => {
const { container } = render(<App />)
const name = container.querySelector('input[name="name"]')
const email = container.querySelector('input[name="email"]')
const color = container.querySelector('input[name="color"]')
const submit = container.querySelector('button[type="submit"]')
})
Let’s change their values
fireEvent.change(name, {
target: {
value: 'mockname'
}
}) fireEvent.change(email, {
target: {
value: 'mockemail'
}
}) fireEvent.change(color, {
target: {
value: 'mockcolor'
}
})
We get this error
Warning: An update to Formik inside a test was not wrapped in act(...).
When testing, code that causes React state updates should be wrapped into act(...):
act(() => {
/* fire events that update state */
});
/* assert on the output */
This means that something changed Formik state inside the test, we need to wait for these changes. act is also good, but instead of act, we can also use Testing Library’s
wait
https://testing-library.com/docs/dom-testing-library/api-async#wait
Notice that we will change the test to
asyncit("submits correct values", async () => {
const { container } = render(<App />)
const name = container.querySelector('input[name="name"]')
const email = container.querySelector('input[name="email"]')
const color = container.querySelector('select[name="color"]')
const submit = container.querySelector('button[type="submit"]')
const results = container.querySelector("textarea"); await wait(() => {
fireEvent.change(name, {
target: {
value: "mockname"
}
})
}) await wait(() => {
fireEvent.change(email, {
target: {
value: "mock@email.com"
}
})
}) await wait(() => {
fireEvent.change(color, {
target: {
value: "green"
}
})
}) await wait(() => {
fireEvent.click(submit)
}) expect(results.innerHTML).toBe(
'{"email":"mock@email.com","name":"mockname","color":"green"}'
)
})
Now we get the correct results and thanks to Testing Library we focus on functionality and DOM behavior instead of dealing with React details.
You can go further and read Formik documentation to create complex forms and Fields. Thanks for reading.
UPDATE: Testing Library ‘wait’ is deprecated now. Use ‘waitFor’ instead.