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>
);
}
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)
}
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) ?? []}
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 ?? ""
);
}}
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
},
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.
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.
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.
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
valuewith the field type in Formik. - Pass an array of values to Autocomplete's
options. - Use
getOptionLabelto display values in a different format than what's inoptions. - Use
nullto represent the "unselected" state in Autocomplete. - MUI provides debounce functionality, eliminating the need for lodash dependency.
- Memoize functions with debounce using
useMemooruseCallback. - Use the
inputValueas the keyword for API search.
I hope this article will be helpful to someone.
Top comments (1)
Nice work! This was a great help. Thank you