Integrating Formik with React Material UI and Typescript

This guide covers one approach for integrating Formik with Material UI in a Typescript project. Formik is a popular form state management library for React.

A codesandbox related to this post is at:
https://codesandbox.io/s/formik-typescript-material-ui-gn692

When integrating Formik with Material UI there are a few approaches that you can take:

  • Defining a custom component that takes advantage of the useField() hook
  • Using Formik’s Field or FastField components and passing your Material UI component via its as, children, or component props

This guide details the latter approach for common/basic form use-cases because it works very well with the Material UI TextField.

The Formik Component

To get started, all forms that are managed by Formik must be wrapped in a <Formik> component. This component receives several props including initialValues and onSubmit.

For validation, you can pass a function to the validate prop that receives the form values. Alternately you can use the yup object schema validation library. The validationSchema prop accepts a yup schema or a function that returns a yup schema.

The <Formik> component uses the render-props pattern to make formikProps available to child components. These include: values, touched, errors, handleChange, handleBlur, isValid, and isSubmitting.

This component is also a context provider that provides form context to its children. Formik includes Field and FastField components that tap into the context. The context can also be accessed by any child component that takes advantage of the useFormikContext() hook.

Refer to the docs for the full suite of options regarding the Formik component.

Start by defining an interface that represents your form values:

interface FormValues {
  name: string;
  description: string;
}

The Formik component can then be used as follows:

      <Formik
        initialValues={{
          name: "",
          description: ""
        }}
        validationSchema={validationSchema} // assuming you have defined one
        onSubmit={(
          values: FormValues,
          formikHelpers: FormikHelpers<FormValues>
        ) => {
          alert(JSON.stringify(values, null, 2));
          formikHelpers.setSubmitting(false);
        }}
      >
        {(formikProps: FormikProps<FormValues>) => (
          ...
          // formikProps includes: values, touched, errors, handleChange, handleBlur, isValid, isSubmitting, etc
          // tip: isSubmitting is useful for disabling the submit button during form submission
          ...
        })
      </Formik>

The initialValues should be of the type FormValues. On submit, the values received will also be of this type.

Note that the onSubmit() function can be async and return a promise. If the function returns a promise, Formik will automatically update the form’s submitting flag to false when the promise resolves/rejects.

The Form Component

All web forms should be wrapped with a <form> tag.

Formik provides a convenient <Form> component that automatically hooks into Formik’s handleSubmit() and handleReset() functions. It is essentially a wrapper for the following:

<form onReset={formikProps.handleReset} onSubmit={formikProps.handleSubmit} {...props}>...</form>

The <Form> component is commonly placed as the first child of the <Formik> component.

Material UI Fields

With the opening Formik and Form components out of the way, we can now specify our form’s fields.

To integrate with Material UI, one approach is to build your own Formik-compatible components with the useField() hook:

const [field, meta, helpers] = useField('example')

The field property holds name, value, and onChange and is designed such that these properties can be passed straight to an input tag.

The meta property holds metadata about the field, including error and touched. These are useful for displaying validation errors, which you may only want to do after your field has been touched by the user.

The helpers are helper functions that enable you to integrate with the formik state: setValue(), setTouched(), and setError().

I think a more straightforward approach, especially for basic/common forms comprised of Material UI TextField components, is to leverage Formik’s <Field> and <FastField> components. These components automatically tap into the context provided by a parent <Formik> component. You supply the name prop and the components hook up the name, value, onChange, and onBlur to an underlying input component.

With Field and FastField you can specify a specific component to render using one of the as, children, or component props. Refer to the docs for the differences between these options.

This guide will focus on using the component prop. This prop accepts either a React component or the name of an HTML element to render, and it passes through all additional props. This prop-passing behaviour makes it ideal for use with a Material UI component because you can also specify any props specific to Material UI.

Material UI includes a helpful <TextField> component that renders a complete form control including a label, an input, and help text. The component and help text is presented with error styles (red outline by default) if the error prop is set to true.

I suggest wrapping the Material UI TextField in your own Formik-friendly component as follows:

import React from 'react'
import { FieldProps, getIn } from 'formik'
import { TextFieldProps, TextField } from '@material-ui/core'

export const AppTextField: React.FC<FieldProps & TextFieldProps> = props => {
  const isTouched = getIn(props.form.touched, props.field.name)
  const errorMessage = getIn(props.form.errors, props.field.name)

  const { error, helperText, field, form, ...rest } = props

  return (
    <TextField
      error={error ?? Boolean(isTouched && errorMessage)}
      helperText={helperText ?? ((isTouched && errorMessage) ? errorMessage : undefined)}
      {...rest} // includes any Material-UI specific props
      {...field} // includes all props contributed by the Formik Field/FastField
    />
  )
}

The above implementation allows you to override the error and helperText props, otherwise it defaults to logic that works well with Formik.

Now, inside your <Form>, you can specify fields as follows:

<Field name="name" component={AppTextField} />
<Field name="description" component={AppTextField} />

You can also include any props specific to customizing the Material UI TextField, e.g.:

<Field name="name" label="Name" variant="outlined" size="small" component={AppTextField} />

Check out the demo on codesandbox for a complete implementation (including validation with yup) to see how everything works including the display of validation errors:

https://codesandbox.io/s/formik-typescript-material-ui-gn692