Handling forms in React could be a pain, Formik is a small library that comes to rescue us. This is not an introduction to formik (if you want one, let me know in the comments) but more of a guide on how to combine custom input components and formik's functionality.
According to their docs Formik helps us:
- Getting values in and out of form state
- Validation and error messages
- Handling form submission
There are a couple ways we can connect Formik to our inputs, let's check them out.
Initial Setup
Here we will use the create-next-app
command, which creates a NextJS app, but feel free to use whatever you like.
In the command line run
npx create-next-app formik-components
Navigate to the app folder
cd formik-components
Let's install formik, yup (for validation) and blueprint (a component library we will use later)
npm install formik yup @blueprintjs/core
Add blueprint's styles inside styles/global.css
@import '~normalize.css';
@import '~@blueprintjs/core/lib/css/blueprint.css';
@import '~@blueprintjs/icons/lib/css/blueprint-icons.css';
Now let's clean up pages/index.js
import Head from 'next/head'
import styles from '../styles/Home.module.css'
export default function Home() {
return (
<div className={styles.container}>
<Head>
<title>Create Next App</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<h1>Hello World</h1>
</div>
)
}
Run the app
npm run dev
We should see something like this
Formik Setup
Let's set up the form and validation we are going to use, these will remain the same throughout this guide.
Inside pages/index.js
add the following code
import Head from 'next/head'
import { Formik, Form, Field, ErrorMessage} from 'formik'
import * as yup from 'yup'
import styles from '../styles/Home.module.css'
export default function Home() {
return (
<div className={styles.container}>
<Head>
<title>Create Next App</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<h1>Hello World</h1>
<Formik
initialValues={{ name: '', email: '' }}
onSubmit={(values) =>
alert(`Thanks for submitting your form ${values.name} `)
}
validationSchema={yup.object().shape({
name: yup.string().required('Name is a required field'),
email: yup
.string()
.email('Not a valid email')
.required('Email is a required field'),
})}
>
{() => (
<Form className={styles.Form}>
{/* Inputs will go here */}
</Form>
)}
</Formik>
</div>
)
}
This is a simple form where the user has to input name and email, although we haven't added inputs yet.
Using <Formik/>
and <Form/>
is my favorite way to use the library, as it reduces a lot of boilerplate.
- Formik: component that helps you with building forms. It uses a render props pattern.
- initialValues: Values to initialize the form, in our case just empty strings form
name
andemail
. - validationSchema: This is where our validation goes, using
yup
we make bothname
andemail
required and add a custom error message.
- initialValues: Values to initialize the form, in our case just empty strings form
- Form: small wrapper around an HTML
<form>
element that automatically hooks into Formik's handleSubmit and handleReset.
Now onto adding input fields!
No Custom Component Approach
If you just want to use good old <input />
then this is what you have to do.
<label htmlFor='name'>Name</label>
<Field
id='name'
name='name'
placeholder='Your Name'
/>
<ErrorMessage
name='name'
/>
So what's going on here, let's look at this <Field />
component
- It's a component that comes from
formik
, it hooks up to our form and it defaults to an HTML<input />
. - You can pass any props you would to a normal
<input />
(className, placeholder, etc). - It needs a
name
prop to know which value to change in our form.
I also added the <ErrorMessage />
component from formik to display the errors, if any, that come from the validations.
The final result should look like this.
<Form className={styles.Form}>
<div className={styles.Fields}>
<div className={styles.FieldContainer}>
<label htmlFor='name'>Name</label>
<Field
id='name'
className={styles.Field}
name='name'
placeholder='Your Name'
/>
<ErrorMessage
component='span'
className={styles.ErrorMessage}
name='name'
/>
</div>
<div className={styles.FieldContainer}>
<label htmlFor='email'>Email</label>
<Field
className={styles.Field}
name='email'
placeholder='Your Email'
/>
<ErrorMessage
component='span'
className={styles.ErrorMessage}
name='email'
/>
</div>
</div>
<button type='submit' className={styles.SubmitButton}>
Submit
</button>
</Form>
I added some <div/>
containers for styling purposes. Feel free to do this however you want. If you want to save time just add this inside styles/Home.module.css
.
.container {
min-height: 100vh;
padding: 0 0.5rem;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.Form {
display: flex;
align-items: center;
flex-direction: column;
background-color: #fff;
width: 30rem;
padding: 4rem 0;
box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.14),
0 2px 1px -1px rgba(0, 0, 0, 0.12), 0 1px 3px 0 rgba(0, 0, 0, 0.2);
border-radius: 0.5rem;
}
.FieldContainer {
display: flex;
flex-direction: column;
margin-bottom: 2.5rem;
min-width: 20rem;
}
.Field {
margin-bottom: 0.2rem;
font-size: 1.2rem;
padding: 0.2rem 0.4rem;
}
.ErrorMessage {
color: red;
font-size: 0.9rem;
}
.SubmitButton {
font-size: 1.2rem;
padding: 0.2rem 2rem;
cursor: pointer;
}
And that's it if you like the simple things, if you want you to use your custom input components keep reading!
Custom Component Approach
Sometimes we use component libraries (Material UI, Ant, Chakra, etc) or our own set of custom components. Input elements are always present and for sure we want to use them in our formik forms. Let's see how we can handle this.
Time to use blueprint, the component library we installed earlier.
The components we will use are:
- FormGroup: Lightweight wrapper around its children with props for the label above and helper text below.
- InputGroup: An input group allows you to add icons and buttons within a text input to expand its functionality, aka fancy
<input />
. - Button: Blueprint's fancy button.
Remember to import these components
Formik's <Field />
component could also be rendered with children
(either JSX or callback function), let's use this to connect formik to our custom components.
<Field
id='name'
className={styles.Field}
name='name'
placeholder='Your Name'
>
{({ field, form, meta }) => (
<FormGroup
label='Name'
helperText={meta.touched && meta.error && meta.error}
intent={meta.touched && meta.error ? 'danger' : 'none'}
labelInfo='(required)'
>
<InputGroup
value={field.value}
onChange={(e) =>
form.setFieldValue(field.name, e.target.value)
}
intent={
meta.touched && meta.error ? 'danger' : 'none'
}
placeholder='Carlos Sanchez'
/>
</FormGroup>
)}
</Field>
Here we are using the callback function, let's analyze the arguments we get from the function.
- field: It contains the field
name
,value
and more. - form: Here we only use it for the
setFieldValue
prop, that sets a field's value (I know, I am a genius). - meta: Contains info about the field, in this case we use it to know if the input has been touched and if it contains validation errors. We use this info to display validations errors in
helperText
and change theintent
of the component.
So the whole thing should look like this
<Form className={styles.Form}>
<div className={styles.Fields}>
<div className={styles.FieldContainer}>
<Field
id='name'
className={styles.Field}
name='name'
placeholder='Your Name'
>
{({ field, form, meta }) => (
<FormGroup
label='Name'
helperText={meta.touched && meta.error && meta.error}
intent={meta.touched && meta.error ? 'danger' : 'none'}
labelInfo='(required)'
>
<InputGroup
value={field.value}
onChange={(e) =>
form.setFieldValue('name', e.target.value)
}
intent={
meta.touched && meta.error ? 'danger' : 'none'
}
placeholder='Carlos Sanchez'
/>
</FormGroup>
)}
</Field>
</div>
<div className={styles.FieldContainer}>
<Field id='email' className={styles.Field} name='email'>
{({ field, form, meta }) => (
<FormGroup
label='Email'
helperText={meta.touched && meta.error && meta.error}
intent={meta.touched && meta.error ? 'danger' : 'none'}
labelInfo='(required)'
>
<InputGroup
value={field.value}
onChange={(e) =>
form.setFieldValue('email', e.target.value)
}
intent={
meta.touched && meta.error ? 'danger' : 'none'
}
placeholder='example@mail.com'
/>
</FormGroup>
)}
</Field>
</div>
</div>
<Button
type='submit'
className={styles.SubmitButton}
intent='primary'
large
fill
>
Submit
</Button>
</Form>
So our form looks more pretty now, but this is really verbose, there has to be a way to make this more DRY.
Reusable Component Approach
So this is basically abstracting all the logic we used above into a reusable component.
In our root folder, let's create a components
folder
mkdir components
Inside the components
folder create a file called CustomInput.js
with the following code.
import { InputGroup, FormGroup } from '@blueprintjs/core';
function CustomInput({
field,
form: { touched, errors },
type = 'text',
label,
labelInfo,
...props
}) {
return (
<FormGroup
label={label}
helperText={
touched[field.name] && errors[field.name] && errors[field.name]
}
intent={touched[field.name] && errors[field.name] ? 'danger' : 'none'}
labelInfo={labelInfo}
>
<InputGroup
intent={touched[field.name] && errors[field.name] ? 'danger' : 'none'}
type={type}
{...field}
{...props}
/>
</FormGroup>
);
}
export default CustomInput;
This component connects the blueprint components and formik with a similar logic we used above. But how is this receiving all the goodies from formik, let's keep reading.
Now back to where we have our form, in the <Field />
component we should pass CustomInput
in the component
prop. This gives access to all the form and field formik props.
<Field
id='name'
className={styles.Field}
name='name'
placeholder='Jhon Doe'
// here
component={CustomInput}
labelInfo='(required)'
label='Name'
/>
The whole thing looks like this now.
<Form className={styles.Form}>
<div className={styles.Fields}>
<div className={styles.FieldContainer}>
<Field
id='name'
className={styles.Field}
name='name'
placeholder='Jhon Doe'
component={CustomInput}
labelInfo='(required)'
label='Name'
/>
</div>
<div className={styles.FieldContainer}>
<Field
id='email'
className={styles.Field}
name='email'
component={CustomInput}
labelInfo='(required)'
label='Email'
placeholder='email@mail.com'
/>
</div>
</div>
<Button
type='submit'
className={styles.SubmitButton}
intent='primary'
large
fill
>
Submit
</Button>
</Form>
Much better, like I said before, here I am using blueprint's components, but you can do this with any other library or your own.
Bonus: useField Component
If you like hooks you can also create your custom input with the useField
hook from formik.
function UseFieldInput({ label, type = 'text', labelInfo, ...props }) {
const [field, meta, helpers] = useField(props.name);
return (
<FormGroup
label={label}
helperText={meta.touched && meta.error && meta.error}
intent={meta.touched && meta.error ? 'danger' : 'none'}
labelInfo={labelInfo}
>
<InputGroup
intent={meta.touched && meta.error ? 'danger' : 'none'}
type={type}
{...field}
{...props}
/>
</FormGroup>
);
}
We can use this component directly, no need of using <Field />
.
<UseFieldInput
id='name'
className={styles.Field}
name='name'
placeholder='Jhon Doe'
labelInfo='(required)'
label='Name'
/>
All together
<Form className={styles.Form}>
<div className={styles.Fields}>
<div className={styles.FieldContainer}>
<UseFieldInput
id='name'
className={styles.Field}
name='name'
placeholder='Jhon Doe'
component={CustomInput}
labelInfo='(required)'
label='Name'
/>
</div>
<div className={styles.FieldContainer}>
<UseFieldInput
id='email'
className={styles.Field}
labelInfo='(required)'
name='email'
component={CustomInput}
label='Email'
placeholder='email@mail.com'
/>
</div>
</div>
<Button
type='submit'
className={styles.SubmitButton}
intent='primary'
large
fill
>
Submit
</Button>
</Form>
Final Words
Hope you found this useful, please like, share and comment if you did.
I also made this live demo to see all the approaches in action.