Enhancing MUI Autocomplete and Formik Integration: Insights and Practical Tips
Tags: react, firmik, mui, tips
Published: 2023-06-26 03:58:22

Photo by <a href="https://unsplash.com/fr/@ilyapavlov?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Ilya Pavlov</a> on <a href="https://unsplash.com/photos/OqtafYT5kTw?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a>

---
  

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.

{% embed https://codesandbox.io/embed/formik-mui-autocomplete-demo-th6f5g?fontsize=14&hidenavigation=1&theme=dark %}

## Code

Here is the code:

```typescript
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>
  );
}
```

## 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`.

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

// ... (snip) ...

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

### 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.

```jsx
options={filteredPostalCodes?.map((p) => p.postalCode) ?? []}
```

### 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`.

```typescript
getOptionLabel={(option: string) => {
  return (
    postalCodes.find((p) => p.postalCode === option)?.address ?? ""
  );
}}
```

### 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.

```typescript
initialValues: {
  postalCode: null
},
```

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. 
```

### 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.

```typescript
import { Autocomplete, TextField, debounce } from "@mui/material";
```

### 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.

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

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.

```typescript
const [filterKeyword, setFilterKeyword] = useState<string>("");
  
// Custom Hook to extract the list of candidates
const { data: filteredPostalCodes } = usePostalCode(filterKeyword);
  
// ... (snip) ...
  
onInputChange={(_, newInputValue) => debouncedSetter(newInputValue)}
```

## 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/