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
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 inoptions
. - 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
oruseCallback
. - Use the
inputValue
as the keyword for API search.
I hope this article will be helpful to someone.
Top comments (0)