Formik

Based on the documentation at Formik.

There are different ways to use Formik, (it also has an extension to Yup). You can use a full component-based approach (it has its own components you can use), or you can be way more programatic and focus entirely on the core functionality attached to your own components (with a hook). We will see all these different "ways" to do it, however the documentation is opinionated and in the end steers you towards a specific approach that has been integrated with Yup plus leverages the component approach.

The first iteration of a really basic Formik implementation would be:

import { useFormik } from 'formik'

function FormikDemo () {
  // Pass the useFormik() hook initial form values and a submit function that will

  // be called when the form is submitted

  const formik = useFormik({
    initialValues: { (1)
      email: ''
    },

    onSubmit: values => { (2)
      alert(JSON.stringify(values, null, 2))
    }
  })

  return (
    <form onSubmit={formik.handleSubmit}> (3)
      <label htmlFor='email'>Email Address</label>
      <input
        id='email'
        name='email'
        type='email'
        onChange={formik.handleChange} (4)
        value={formik.values.email}
      />

      <button type='submit'>Submit</button>
    </form>
  )
}

export { FormikDemo }
1 It is through the initialValues property of the parameter for the useFormik hook that we can declare the shape of the whole object the form will capture.
2 And it is through the onSubmit property we can feed a function that will run the moment a submit on the form is triggered.
3 As you can see we can then start consuming the result from the hook in the respective places.
4 We can drill down all the way to the property names.

It is worth noting that even without some form of validation explicitly stated here, Formik detects the type of the input and in this case, if we write a malformed email it will show an error there specific to that rule.

As a second step we will add two more fields to the form (think of it as a new requirement that just came in):

import { useFormik } from 'formik'

function FormikDemo () {
  // Pass the useFormik() hook initial form values and a submit function that will

  // be called when the form is submitted

  const formik = useFormik({
    initialValues: {
      firstName: '',
      lastName: '',
      email: ''
    },

    onSubmit: values => {
      alert(JSON.stringify(values, null, 2))
    }
  })

  return (
    <form onSubmit={formik.handleSubmit}>
      <label htmlFor='firstName'>First Name</label>

      <input
        id='firstName'
        name='firstName'
        type='text'
        onChange={formik.handleChange}
        value={formik.values.firstName}
      />

      <label htmlFor='lastName'>Last Name</label>

      <input
        id='lastName'
        name='lastName'
        type='text'
        onChange={formik.handleChange}
        value={formik.values.lastName}
      />

      <label htmlFor='email'>Email Address</label>

      <input
        id='email'
        name='email'
        type='email'
        onChange={formik.handleChange}
        value={formik.values.email}
      />

      <button type='submit'>Submit</button>
    </form>
  )
}

export { FormikDemo }

If you look carefully, you’ll notice some patterns and symmetry forming.

  1. We reuse the same exact change handler function handleChange for each HTML input.

  2. We pass an id and name HTML attribute that matches the property we defined in initialValues

  3. We access the field’s value using the same name (email→`formik.values.email`)

For people familiar with forms in plain React, Formik’s handleChange works the same way as you would with code like:

const [values, setValues] = React.useState({})

const handleChange = event => {
  setValues(prevValues => ({
    ...prevValues,
    [event.target.name]: event.target.value
  })
}

Validation

HTML’s input native properties such as required, maxlength, minlength and pattern for regex are a way to validate. However HTML has its limitations, e.g. it only works on browser, React Native is not viable, it’s also impossible to show custom error messages. And it’s very janky.

Formik keeps track not only of the form’s values but also its validation and error messages. To add validation with JS, we can specify a custom validation function and pass it as validate inside of the hook. If an error exists, the custom validation function should produce an error object with a matching shape to our values / initialValues. Symmetry.

formik.errors are populated via the custom validation function. By default, Formik will validate after each keystroke (change event), each input’s blur event, as well as prior to submission. The onSubmit function we passed to useFormik() will be executed only if there are no errors (i.e. if our validate function returns {}).

Meaning that if we type and the resulting string has an error, it will immediately show, before submitting it won’t allow for us to do it unless all values are valid, and the moment we focus an input is also taken into account to render errors.

import { useFormik } from 'formik'

interface FormProps { (3)
  firstName: string,
  lastName: string,
  email: string
}

// A custom validation function. This must return an object
// which keys are symmetrical to our values/initialValues
const validate = (values: FormProps) => { (1)
  const errors: Partial<FormProps> = {} (2)

  if (!values.firstName) {
    errors.firstName = 'Required'
  } else if (values.firstName.length > 15) {
    errors.firstName = 'Must be 15 characters or less'
  }

  if (!values.lastName) {
    errors.lastName = 'Required'
  } else if (values.lastName.length > 20) {
    errors.lastName = 'Must be 20 characters or less'
  }

  if (!values.email) {
    errors.email = 'Required'
  } else if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(values.email)) {
    errors.email = 'Invalid email address'
  }

  return errors (4)
}

function FormikDemo () {
  /// Pass the useFormik() hook initial form values, a validate function that will be called when
  // form values change or fields are blurred, and a submit function that will
  // be called when the form is submitted
  const formik = useFormik({
    initialValues: {
      firstName: '',
      lastName: '',
      email: ''
    },
    validate, (5)
    onSubmit: values => {
      alert(JSON.stringify(values, null, 2))
    }
  })

  return (
    <form onSubmit={formik.handleSubmit}>
      <label htmlFor='firstName'>First Name</label>

      <input
        id='firstName'
        name='firstName'
        type='text'
        onChange={formik.handleChange}
        value={formik.values.firstName}
      />
      {formik.errors.firstName ? <div>{formik.errors.firstName}</div> : null} (6)

      <label htmlFor='lastName'>Last Name</label>

      <input
        id='lastName'
        name='lastName'
        type='text'
        onChange={formik.handleChange}
        value={formik.values.lastName}
      />
      {formik.errors.lastName ? <div>{formik.errors.lastName}</div> : null}

      <label htmlFor='email'>Email Address</label>

      <input
        id='email'
        name='email'
        type='email'
        onChange={formik.handleChange}
        value={formik.values.email}
      />
      {formik.errors.email ? <div>{formik.errors.email}</div> : null}

      <button type='submit'>Submit</button>
    </form>
  )
}

export { FormikDemo }
1 In order to follow best practices the values object was typed.
2 The errors object will also hold the same properties as the main interface, with the possibility to not have the same properties, hence the utility Partial class was used.
3 This is the declaration of the interface for the shape of the form object.
4 By the end, we will return an errors object, and if it has no properties (meaning no errors) then Formik will allow for the form to submit.
5 We are using the same name for the validation function as the property the hook expects, hence we can write it in its simplified form.
6 And lastly, you can see how we leverage conditional rendering in order to show the error message respective to the input only if the error object has an error registered for the respective property name.

Visited fields

One fault we can see right now is the fact that the validation function runs on each keystroke against the entire form’s values, our error object contains all validation errors at any given moment. And we are currently checking if an error exists and then immediately showing it to the user. It’s not a good UX if the user sees an error the moment the form loads, usually we’d want to wait for the user to interact with the control to then show errors.

Formik has a way to access a touched value, it will be mapped to the respective control name and then be open for us to consume it. It will also be updated based on the onBlur property the control can tie in with Formik’s handleBlur

import { useFormik } from 'formik'

interface FormProps {
  firstName: string
  lastName: string
  email: string
}

// A custom validation function. This must return an object
// which keys are symmetrical to our values/initialValues
const validate = (values: FormProps) => {
  const errors: Partial<FormProps> = {}

  if (!values.firstName) {
    errors.firstName = 'Required'
  } else if (values.firstName.length > 15) {
    errors.firstName = 'Must be 15 characters or less'
  }

  if (!values.lastName) {
    errors.lastName = 'Required'
  } else if (values.lastName.length > 20) {
    errors.lastName = 'Must be 20 characters or less'
  }

  if (!values.email) {
    errors.email = 'Required'
  } else if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(values.email)) {
    errors.email = 'Invalid email address'
  }

  return errors
}

function FormikDemo () {
  /// Pass the useFormik() hook initial form values, a validate function that will be called when
  // form values change or fields are blurred, and a submit function that will
  // be called when the form is submitted
  const formik = useFormik({
    initialValues: {
      firstName: '',
      lastName: '',
      email: ''
    },
    validate,
    onSubmit: values => {
      alert(JSON.stringify(values, null, 2))
    }
  })

  return (
    <form onSubmit={formik.handleSubmit}>
      <label htmlFor='firstName'>First Name</label>

      <input
        id='firstName'
        name='firstName'
        type='text'
        onChange={formik.handleChange}
        onBlur={formik.handleBlur} (1)
        value={formik.values.firstName}
      />
      {formik.touched.firstName && formik.errors.firstName ? ( (2)
        <div>{formik.errors.firstName}</div>
      ) : null}

      <label htmlFor='lastName'>Last Name</label>

      <input
        id='lastName'
        name='lastName'
        type='text'
        onChange={formik.handleChange}
        onBlur={formik.handleBlur}
        value={formik.values.lastName}
      />
      {formik.touched.lastName && formik.errors.lastName ? (
        <div>{formik.errors.lastName}</div>
      ) : null}

      <label htmlFor='email'>Email Address</label>

      <input
        id='email'
        name='email'
        type='email'
        onChange={formik.handleChange}
        onBlur={formik.handleBlur}
        value={formik.values.email}
      />
      {formik.touched.email && formik.errors.email ? (
        <div>{formik.errors.email}</div>
      ) : null}

      <button type='submit'>Submit</button>
    </form>
  )
}

export { FormikDemo }
1 We simply have to add to the control’s onBlur property Formik’s handleBlur function. This will take care of figuring out which control this belongs to, and update a touched value for it respectively.
2 And when trying to display the error message, we can now leverage Formik’s touched object that will have the same properties as the ones that are in initialValues / values. If the specific control was interacted with then touched for that specific field would yield true.

Schema Validation with Yup

Validation can be left up to you. You can write your own validators or use a 3rd-party helper library. Lots of people using Formik prefer using Yup for object schema validation. Yup has an API similar to Joi and React PropTypes, but it’s also small enough for the browser and fast enough for runtime usage.

Integration with Yup is seamless, you just need to install the library and then make use of the validationSchema property which will automatically transform Yup’s validation error messages into a pretty object whose keys match values/initialValues/touched.

pnpm i yup

To see how Yup can work, we can get rid of the validate function and re-write validation with Yup and validationSchema.

const formik = useFormik({
    initialValues: {
      firstName: '',
      lastName: '',
      email: ''
    },
    validationSchema: Yup.object({ (1)
      firstName: Yup.string() (2)
        .max(15, 'Must be 15 characters or less') (3)
        .required('Required'), (4)

      lastName: Yup.string()
        .max(20, 'Must be 20 characters or less')
        .required('Required'),

      email: Yup.string()
        .email('Invalid email address')
        .required('Required')
    }),
    onSubmit: values => {
      alert(JSON.stringify(values, null, 2))
    }
  })
1 We must leverage a Yup object. You can import everything from the library by doing a import * as Yup from 'yup'.
2 And inside, the object will declare the properties that should be respective to the shape of the form we will map its validations to.
3 As you can see it follows a fluent pattern, it declares the type of value it will hold and for different criteria you can declare what error message will be shown.
4 Even for required, you can customize the error message there.

Yup is 100% optional. But it’s recommended. Formik’s core design principles are in place to help you keep organized. Schemas are extremely expressive, intuitive and reusable. With or without Yup it’s highly advisable to share commonly used validation methods across the application. This will ensure that common fields are validated consistently and result in a better user experience.

Reducing Boilerplate

getFieldProps

The code so far is pretty explicit (imperative) of what it does. Defining every single property onChangehandleChange, onBlurhandleBlur. However useFormik() returns a helper method called formik.getFieldProps() to make wiring of inputs faster. Given a field-level info it will return onChange, onBlur, value and checked for a given field. You can then spread that on any input, select or textarea.

<input
    id='firstName'
    type='text'
    {...formik.getFieldProps('firstName')} (1)
/>
1 Pretty straightforward, the pattern simply requires for you to state the id and the type properties, right after that you can simply spread the fieldname result for the helper function. The form will work the exact same whilst being more declarative.

Leveraging React Context

Code is more concise, yet it still is pretty explicit. Formik comes in React-Context- powered API/components to make things even less verbose. They use React Context implicitly to connect with the Parent <Formik /> state/methods. A manual approach would work like this:

import React from 'react';
import { useFormik } from 'formik';

// Create empty context
const FormikContext = React.createContext({});

// Place all of what's returned by useFormik into context
export const Formik = ({ children, ...props }) => {
    const formikStateAndHelpers = useFormik(props);

    return (
        <FormikContext.Provider value={formikStateAndHelpers}>
        {typeof children === 'function'
            ? children(formikStateAndHelpers)
            : children}
        </FormikContext.Provider>
        );
};

But, we don’t need that, since there’s a Formik component that comes with all of that baked into it.

import { Formik } from 'formik'
import * as Yup from 'yup'

function FormikDemo () {
  return (
    <Formik
      initialValues={{ (1)
        firstName: '',
        lastName: '',
        email: ''
      }}
      validationSchema={Yup.object({ (2)
        firstName: Yup.string()
          .max(15, 'Must be 15 characters or less')
          .required('Required'),

        lastName: Yup.string()
          .max(20, 'Must be 20 characters or less')
          .required('Required'),

        email: Yup.string().email('Invalid email address').required('Required')
      })}
      onSubmit={(values, { setSubmitting }) => { (3)
        setTimeout(() => {
          alert(JSON.stringify(values, null, 2))
          setSubmitting(false)
        }, 400)
      }}
    >
      {formik => ( (4)
        <form onSubmit={formik.handleSubmit}>
          <label htmlFor='firstName'>First Name</label>

          <input
            id='firstName'
            type='text'
            {...formik.getFieldProps('firstName')}
          />
          {formik.touched.firstName && formik.errors.firstName ? (
            <div>{formik.errors.firstName}</div>
          ) : null}

          <label htmlFor='lastName'>Last Name</label>

          <input
            id='lastName'
            type='text'
            {...formik.getFieldProps('lastName')}
          />
          {formik.touched.lastName && formik.errors.lastName ? (
            <div>{formik.errors.lastName}</div>
          ) : null}

          <label htmlFor='email'>Email Address</label>

          <input id='email' type='email' {...formik.getFieldProps('email')} />
          {formik.touched.email && formik.errors.email ? (
            <div>{formik.errors.email}</div>
          ) : null}

          <button type='submit'>Submit</button>
        </form>
      )}
    </Formik>
  )
}

export { FormikDemo }
1 As you can see the component gets in its properties the same properties that the hook did before, such as the initial values that gives the shape of the object.
2 validationSchema is also something you feed as a property.
3 A onSubmit property also receives a function, the function will receive as a first parameter the values, and as a second parameter an object that holds properties with utility functions, such as setSubmitting() that will update the internal state of the form (in terms of if it’s under submission or not)
4 And the children "elements" of the Formik component will be another function, in this specific instance it’s a render prop. A technique to send a render function to a component so that it doesn’t implement the rendering on his own and instead the consumer sends said rendering logic. (Pretty cool)

The form works the same as before, however in a more concise manner.

Now, we can make it even more concise by leveraging a <Field> component that Formik also has.

{formik => (
    <form onSubmit={formik.handleSubmit}>
        <label htmlFor='firstName'>First Name</label>
        <Field name='firstName' type='text' /> (1)
        <ErrorMessage name='firstName' /> (2)

        <label htmlFor='lastName'>Last Name</label>
        <Field name='lastName' type='text' />
        <ErrorMessage name='lastName' />

        <label htmlFor='email'>Email Address</label>
        <Field name='email' type='text' />
        <ErrorMessage name='email' />

        <button type='submit'>Submit</button>
    </form>
)}
1 The <Field> component by default will render an <input> component given that, a name prop, will implicitly grab the respective onChange, onBlur, value props and pass them to the element as well as any props you pass to it.
2 And we also can leverage an <ErrorMessage> component that will abstract all the logic of touched, plus wiring it to the respective value and error of the form property.

These components hold inside of them different properties that allow for you to shape behaviors as you might need, amongst styling, and the type of input.

// <input className="form-input" placeHolder="Jane"  />
<Field name="firstName" className="form-input" placeholder="Jane" />

// <textarea className="form-textarea"/></textarea>

<Field name="message" as="textarea" className="form-textarea" />

// <select className="my-select"/>

<Field name="colors" as="select" className="my-select">
    <option value="red">Red</option>
    <option value="green">Green</option>
    <option value="blue">Blue</option>
</Field>

As you can see, you can cover all basic forms of inputs such as textarea, select, and even accept styling for the component.

This is all good, we got rid of prop-drilling, but we are still repeating ourselves with a label, Field and ErrorMessage for each of our inputs. We can do better with an abstraction. With Formik, you can and should build reusable input primitive components that you can share around your app. The Field render-prop component has a hook equivalent called useField.

This would be a recommended approach of leveraging Formik with Typescript and its capabilities:

import { Form, Formik, useField } from 'formik'
import { ReactNode } from 'react'
import * as Yup from 'yup'

interface InputProps { (1)
  label: string
  name: string
  id?: string
  type?: string
  placeholder?: string
}

interface ChildrenInputProps extends InputProps {
  children: ReactNode (2)
}

interface CheckboxProps extends Omit<ChildrenInputProps, 'label'> { (3)
  label?: string
}

const MyTextInput = ({ label, ...props }: InputProps) => {
  // useField() returns [formik.getFieldProps(), formik.getFieldMeta()]
  // which we can spread on <input>. We can use field meta to show an error
  // message if the field is invalid and it has been touched (i.e. visited)
  const [field, meta] = useField(props) (4)

  return (
    <>
      <label htmlFor={props.id || props.name}>{label}</label>

      <input className='text-input' {...field} {...props} />

      {meta.touched && meta.error ? (
        <div className='error'>{meta.error}</div>
      ) : null}
    </>
  )
}

const MyCheckbox = ({ children, ...props }: CheckboxProps) => {
  // React treats radios and checkbox inputs differently from other input types:
  // select and textarea.
  // Formik does this too! When you specify `type` to useField(), it will
  // return the correct bag of props for you -- a `checked` prop will be included
  // in `field` alongside `name`, `value`, `onChange`, and `onBlur`
  const [field, meta] = useField({ ...props, type: 'checkbox' })

  return (
    <div>
      <label className='checkbox-input'>
        <input type='checkbox' {...field} {...props} />
        {children} (5)
      </label>

      {meta.touched && meta.error ? (
        <div className='error'>{meta.error}</div>
      ) : null}
    </div>
  )
}

const MySelect = ({ label, ...props }: ChildrenInputProps) => {
  const [field, meta] = useField(props)

  return (
    <div>
      <label htmlFor={props.id || props.name}>{label}</label>
      <select {...field} {...props} />
      {meta.touched && meta.error ? (
        <div className='error'>{meta.error}</div>
      ) : null}
    </div>
  )
}

function FormikDemo () {
  return (
    <Formik
      initialValues={{
        firstName: '',
        lastName: '',
        email: ''
      }}
      validationSchema={Yup.object({
        firstName: Yup.string()
          .max(15, 'Must be 15 characters or less')
          .required('Required'),

        lastName: Yup.string()
          .max(20, 'Must be 20 characters or less')
          .required('Required'),

        email: Yup.string().email('Invalid email address').required('Required')
      })}
      onSubmit={(values, { setSubmitting }) => {
        setTimeout(() => {
          alert(JSON.stringify(values, null, 2))
          setSubmitting(false)
        }, 400)
      }}
    >
      <Form> (6)
        <MyTextInput
          label='First Name'
          name='firstName'
          type='text'
          placeholder='Jane'
        />

        <MyTextInput
          label='Last Name'
          name='lastName'
          type='text'
          placeholder='Doe'
        />

        <MyTextInput
          label='Email Address'
          name='email'
          type='email'
          placeholder='jane@formik.com'
        />

        <MySelect label='Job Type' name='jobType'>
          <option value=''>Select a job type</option>
          <option value='designer'>Designer</option>
          <option value='development'>Developer</option>
          <option value='product'>Product Manager</option>
          <option value='other'>Other</option>
        </MySelect>

        <MyCheckbox name='acceptedTerms'>
          I accept the terms and conditions
        </MyCheckbox>

        <button type='submit'>Submit</button>
      </Form>
    </Formik>
  )
}

export { FormikDemo }
1 First of all, we are making use of Typescript, so we will enforce types whenever we need to so that both intellisense and linters make our dev effort easier.
2 If we have to type children properties, meaning other component inside of the same component we can type it with ReactNode
3 Never forget about Typescript’s utility types, this one will inherit everything from ChildrenInputProps but it will gouge out label, and right after we are adding it as an optional field back.
4 useField is a hook that will return all the neccesary properties but in an abstracted form in order to hook Formik to input properties. You can see how its second item of the resulting array is an object used to leverage the status of the field in terms of touched, and error. The hook will result in errors if you are not careful enough, so be sure to type the props correctly.
5 As you can see, children properties are ones that can be received at the component props, and then passed down to other component so that they can be rendered.
6 Another utility component is Form, just as the other components we saw this will leverage everything internally, a much more declarative approach that under hood leverages many features in order to make the form work accordingly.

Just a last extra piece of info in here, if you want to be more optimal in a way, and not try to validate on each keystroke, you can easily configure this on formik’s arguments:

const formik = useFormik({
  initialValues: { name: '' },
  validate: values => {
    const errors = {};
    if (!values.name) {
      errors.name = 'Required';
    }
    return errors;
  },
  validateOnChange: false, // Disable validation on change
  validateOnBlur: false,   // Disable validation on blur
  onSubmit: values => {
    console.log('Submitted values:', values);
  }
});

If we don’t want for the frontend to carry validation logic and be as dumb as possible, we could levarage a network call to the backend in order for all logic to be centralized there. Now, this is where depending on the depth of the validation we would have to make databae calls and all. But caching can ease the load on the server for simple validations, then again if validations only live at memory level, we should be good. But even then we could add mechanisms to avoid overflowing the service with validation calls.