DEV Community

Cover image for Enhancing MUI Autocomplete and Formik Integration: Insights and Practical Tips
Shinji NAKAMATSU
Shinji NAKAMATSU Subscriber

Posted on

Enhancing MUI Autocomplete and Formik Integration: Insights and Practical Tips

Photo by Ilya Pavlov on Unsplash


I wanted to do something like this for work, but I couldn't find a good sample on the internet, so I had to experiment and figure it out. So, I'm publishing it as a blog post.

Demo

First, let me show you the demo.

Assuming a form where users enter a postal code in Tokyo, I have implemented an Autocomplete component that displays and allows the selection of address candidates based on keywords. (I have extracted only a few postal codes as including all of them would result in too many options)

When you press the Submit button, the contents of the form are outputted in JSON format.

Code

Here is the code:

import { SyntheticEvent, useMemo, useCallback, useState } from "react";
import "./styles.css";
import { useFormik } from "formik";
import { Autocomplete, TextField, debounce } from "@mui/material";
import { postalCodes, usePostalCode } from "./data";

type FormValues = {
  postalCode: string | null;
};

export default function App() {
  const [filterKeyword, setFilterKeyword] = useState<string>("");
  const [submitCode, setSubmitCode] = useState<string>("");

  // Custom Hook to extract the list of candidates
  const { data: filteredPostalCodes } = usePostalCode(filterKeyword);

  const formik = useFormik<FormValues>({
    initialValues: {
      postalCode: null
    },
    onSubmit: (values) => {
      console.log(JSON.stringify(values));
      setSubmitCode(JSON.stringify(values));
    }
  });

  const debouncedSetter = useMemo(
    () => debounce((keyword: string) => setFilterKeyword(keyword), 500),
    []
  );

  return (
    <div className="App">
      <h1>Formik + MUI Autocomplete Demo</h1>
      <form onSubmit={formik.handleSubmit}>
        <label htmlFor="postalCode">Postal Code: </label>
        <Autocomplete
          value={formik.values.postalCode}
          onChange={(_: SyntheticEvent, newValue: string | null) =>
            formik.setFieldValue("postalCode", newValue)
          }
          options={filteredPostalCodes?.map((p) => p.postalCode) ?? []}
          onInputChange={(_, newInputValue) => debouncedSetter(newInputValue)}
          // filterOptions={(x) => x} // Disable the default filtering as we handle it ourselves
          getOptionLabel={(option: string) => {
            return (
              postalCodes.find((p) => p.postalCode === option)?.address ?? ""
            );
          }}
          renderInput={(params) => (
            <TextField {...params} placeholder="Postal Code" id="" />
          )}
        />
        <button type="submit">Submit</button>
      </form>
      <div>{submitCode && <>Submit code: {submitCode}</>}</div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Implementation Points

There are several implementation points that I want to explain briefly.

Match the type of Autocomplete's value with the field type in Formik

When using Autocomplete to input values into a Formik form, it is easier to handle if the types of each value match.

In the sample code, I created a form that allows users to input values into the postalCode field in Formik using Autocomplete. In this case, both the type of values.postalCode in Formik and the type of value in Autocomplete are implemented as string.

type FormValues = {
  postalCode: string | null;
};

// ... (snip) ...

<Autocomplete
  loading={isPostalCodeLoading}
  value={formik.values.postalCode} 
  onChange={(_: SyntheticEvent, newValue: string | null) =>
    formik.setFieldValue("postalCode", newValue)
  }
Enter fullscreen mode Exit fullscreen mode

Pass an array of values to Autocomplete's options

Intuitively, we might assume that options should contain the strings displayed in the Autocomplete's dropdown list. However, it's a bit different.

In options, you should pass an array of values that will be used as the selected value assigned to value.

In the sample code, we assume that the postalCode value will be assigned to value, so we set an array consisting of postalCode as the elements.

options={filteredPostalCodes?.map((p) => p.postalCode) ?? []}
Enter fullscreen mode Exit fullscreen mode

Use getOptionLabel to display values in a different format than what's in options

As mentioned earlier, the values set in options may not be user-friendly or easily understandable, so it's necessary to convert them into a more readable (and selectable) format.

In the sample code, we pass an array of postalCode to options, but we decided to display the corresponding address in the UI.

The implementation of the function takes an argument option, which is an element (i.e., postalCode) of options. We then find the corresponding element in the postalCodes array and return its address.

getOptionLabel={(option: string) => {
  return (
    postalCodes.find((p) => p.postalCode === option)?.address ?? ""
  );
}}
Enter fullscreen mode Exit fullscreen mode

Use null to represent the "unselected" state in Autocomplete

The value that represents the "unselected" state in Autocomplete (i.e., value) is null. Therefore, in the initialValues of Formik, we set null as the initial value.

initialValues: {
  postalCode: null
},
Enter fullscreen mode Exit fullscreen mode

If you mistakenly set "" (an empty string) instead, you will see a warning like this:

MUI: The value provided to Autocomplete is invalid.
None of the options match with `""`.
You can use the `isOptionEqualToValue` prop to customize the equality test. 
Enter fullscreen mode Exit fullscreen mode

MUI provides debounce functionality

While many articles suggest using lodash or implementing debounce manually, MUI actually provides debounce functionality, so you don't need to add a dependency on lodash or implement it yourself.

import { Autocomplete, TextField, debounce } from "@mui/material";
Enter fullscreen mode Exit fullscreen mode

Memoize functions with debounce using useMemo or useCallback

It's easy to overlook, but functions wrapped with debounce need to be memoized to work correctly.

const debouncedSetter = useMemo(
  () => debounce((keyword: string) => setFilterKeyword(keyword), 500),
  []
);
Enter fullscreen mode Exit fullscreen mode

Typically, you would use the useCallback hook to memoize a function. However, ESLint raises a warning saying "React Hook useCallback received a function whose dependencies are unknown. Pass an inline function instead. (react-hooks/exhaustive-deps)." Therefore, we use useMemo instead.

Use inputValue as the keyword for API search

In the sample code, the custom hook usePostalCode is designed to make an API call and retrieve a list of postal codes and their corresponding addresses based on the keyword provided.

const [filterKeyword, setFilterKeyword] = useState<string>("");

// Custom Hook to extract the list of candidates
const { data: filteredPostalCodes } = usePostalCode(filterKeyword);

// ... (snip) ...

onInputChange={(_, newInputValue) => debouncedSetter(newInputValue)}
Enter fullscreen mode Exit fullscreen mode

Conclusion

With the combination of Formik and Autocomplete, we were able to create a form with autocomplete functionality.

To summarize the key points:

  • Match the type of Autocomplete's value with the field type in Formik.
  • Pass an array of values to Autocomplete's options.
  • Use getOptionLabel to display values in a different format than what's in options.
  • Use null to represent the "unselected" state in Autocomplete.
  • MUI provides debounce functionality, eliminating the need for lodash dependency.
  • Memoize functions with debounce using useMemo or useCallback.
  • Use the inputValue as the keyword for API search.

I hope this article will be helpful to someone.

Reference Links

https://formik.org/docs/api/formik

https://mui.com/material-ui/api/autocomplete/

Top comments (0)