I'm trying to implement debounce
for mui autocomplete,
but it's not working well.
I want to send a debounced request to server when inputValue
change.
Am I miss something?
It looks loadData
fired every input change. Only first load debounce
works.
https://codesandbox.io/s/debounce-not-working-j4ixgg?file=/src/App.js
Here's the code from the sandbox:
import { useState, useCallback } from "react";
import { Autocomplete, TextField } from "@mui/material";
import { debounce } from "lodash";
import topFilms from "./topFilms";
export default function App() {
const [value, setValue] = useState(null);
const [inputValue, setInputValue] = useState("");
const [options, setOptions] = useState([]);
const loadData = () => {
// sleep(1000)
const filteredOptions = topFilms.filter((f) =>
f.title.includes(inputValue)
);
// This log statement added by Ryan Cogswell to show why it isn't working.
console.log(
`loadData with ${filteredOptions.length} options based on "${inputValue}"`
);
setOptions(filteredOptions);
};
const debouncedLoadData = useCallback(debounce(loadData, 1000), []);
const handleInputChange = (e, v) => {
setInputValue(v);
debouncedLoadData();
};
const handleChange = (e, v) => {
setValue(v);
};
return (
<div className="App">
<Autocomplete
value={value}
inputValue={inputValue}
onChange={handleChange}
onInputChange={handleInputChange}
disablePortal
options={options}
getOptionLabel={(option) => option.title}
isOptionEqualToValue={(option, value) => option.title === value.title}
id="combo-box-demo"
renderInput={(params) => <TextField {...params} label="Movie" />}
/>
</div>
);
}
You can accomplish this with a few hooks working together. First lets look at a hook for debouncing state:
function useDebounce(value, delay, initialValue) {
const [state, setState] = useState(initialValue);
useEffect(() => {
console.log("delaying", value);
const timer = setTimeout(() => setState(value), delay);
// clear timeout should the value change while already debouncing
return () => {
clearTimeout(timer);
};
}, [value, delay]);
return state;
}
The above takes in a value and returns the debounced value from the hook later. The callback at the end of the useEffect prevents a bunch of timers triggering one after the other.
Then your component can be reduced to:
export default function App() {
const [value, setValue] = useState(null);
const [inputValue, setInputValue] = useState("");
const [options, setOptions] = useState([]);
const debouncedValue = useDebounce(inputValue, 1000);
// fetch data from server
useEffect(() => {
console.log("fetching", debouncedValue);
const filteredOptions = topFilms.filter((f) =>
f.title.includes(debouncedValue)
);
setOptions(filteredOptions);
}, [debouncedValue]);
const handleInputChange = (e, v) => {
setInputValue(v);
};
const handleChange = (e, v) => {
setValue(v);
};
return (
<div className="App">
<Autocomplete
// props
/>
</div>
);
}
The useEffect here is run when the dependency debouncedValue
is changed via react state business.
When typing in the TextField your console should look like:
delaying s
delaying sp
delaying spi
fetching spi
delaying spir
fetching spir
This useEffect
in App
leaves you with a good place to do your fetch to a server like you mentioned you'll need.
The useEffect
could easily be replaced with something like useQuery