Creating Reusable Input Components with Formik

Creating Reusable Input Components with Formik

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

Screenshot from 2020-08-23 21-38-25.png

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 and email.
    • validationSchema: This is where our validation goes, using yup we make both name and email required and add a custom error message.
  • 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 the intent 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.