I am using the Material-UI Autocomplete component for a search box but am running into an issue. The options are fetched from an API and passed to the component. The first time I open the dropdown, the list of options appears as expected. However, when I close and open the dropdown again, it shows no options and the loading icon appears continuously. My desired functionality is that the user can fetch the options at the beginning, then when typing, the options are refetched with a loading icon present. When there are no results, it should return "No options found".
Sandbox: https://codesandbox.io/s/autocomplete-with-typescript-forked-3l2777?file=/src/App.tsx
import * as React from "react";
import { Autocomplete, TextField } from "@mui/material";
import SearchIcon from "@material-ui/icons/Search";
import InputAdornment from "@material-ui/core/InputAdornment";
import CircularProgress from "@material-ui/core/CircularProgress";
interface Option {
label: string;
value: number;
}
interface IProps {
fetchServerSideOptions?: (inputValue: string) => void;
loading?: boolean;
open?: boolean;
onOpen?: (event: React.SyntheticEvent) => void;
onClose?: (event: React.SyntheticEvent, reason: string) => void;
options: Option[];
selected: Option;
setSelected: React.Dispatch<React.SetStateAction<Option>>;
}
export default function Auto(props: IProps) {
const {
options = [],
selected = [],
setSelected,
open,
loading,
onOpen,
onClose,
fetchServerSideOptions
} = props;
return (
<Autocomplete
forcePopupIcon={false}
options={options}
value={selected}
open={open}
loading={loading}
onOpen={onOpen}
onClose={onClose}
onChange={(_event, value) => setSelected(value)}
onInputChange={(_event, value) => fetchServerSideOptions?.(value)}
renderInput={(params) => {
return (
<TextField
{...params}
placeholder="Select"
InputProps={{
...params.InputProps,
startAdornment: (
<React.Fragment>
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
{params.InputProps.startAdornment}
</React.Fragment>
),
endAdornment: (
<React.Fragment>
{loading ? (
<CircularProgress color="inherit" size={20} />
) : null}
{params.InputProps.endAdornment}
</React.Fragment>
)
}}
/>
);
}}
/>
);
}
import { useState, useEffect } from "react";
import Auto from "./Auto";
interface Option {
label: string;
value: number;
}
export default function App() {
const [options, setOptions] = useState<Option[]>([]);
const [selected, setSelected] = useState<Option | null>(null);
const [open, setOpen] = useState<boolean>(false);
const loading = open && options.length === 0;
const fetchOptions = async (value: string) => {
const response: Response = await fetch(
`https://dummyjson.com/products/search?q=${value}&limit=10`
);
const { products } = await response.json();
const formattedOptions = products.map(({ title, id }) => {
return { label: title, value: id };
});
console.log(formattedOptions);
setOptions(formattedOptions);
};
useEffect(() => {
fetchOptions("");
}, []);
useEffect(() => {
if (!open) {
setOptions([]);
}
}, [open]);
return (
<Auto
selected={selected}
setSelected={setSelected}
options={options}
open={open}
loading={loading}
onOpen={() => setOpen(true)}
onClose={() => setOpen(false)}
fetchServerSideOptions={(value) => fetchOptions(value)}
/>
);
}
>Solution :
You can remove second useEffect that is not required.
useEffect(() => {
if (!open) {
setOptions([]);
}
}, [open]);
Add a state which shows when it is loading(when API call is going on) not when length is 0
const [loading, setLoading] = useState(false);
// const loading = open && options.length === 0;
export default function App() {
const [options, setOptions] = useState<Option[]>([]);
const [selected, setSelected] = useState<Option | null>(null);
const [open, setOpen] = useState<boolean>(false);
const [loading, setLoading] = useState(false);
// const loading = open && options.length === 0;
const fetchOptions = async (value: string) => {
try {
setLoading(true); // CHANGE
const response: Response = await fetch(
`https://dummyjson.com/products/search?q=${value}&limit=10`
);
const { products } = await response.json();
const formattedOptions = products.map(({ title, id }) => {
return { label: title, value: id };
});
console.log(formattedOptions);
setOptions(formattedOptions);
} catch (error) {
console.error(error);
} finally {
setLoading(false); // CHANGE
}
};
useEffect(() => {
fetchOptions("");
}, []);
// useEffect(() => {
// if (!open) {
// setOptions([]);
// }
// }, [open]);
return (
<Auto
selected={selected}
setSelected={setSelected}
options={options}
open={open}
loading={loading}
onOpen={() => setOpen(true)}
onClose={() => setOpen(false)}
fetchServerSideOptions={(value) => fetchOptions(value)}
/>
);
}