[英]Debouncing MUI Autocomplete not working properly
I'm trying to implement debounce
for mui autocomplete, but it's not working well.我正在尝试为 mui 自动完成实现debounce
,但效果不佳。
I want to send a debounced request to server when inputValue
change.我想在inputValue
更改时向服务器发送去抖请求。
Am I miss something?我想念什么吗?
It looks loadData
fired every input change.看起来loadData
触发了每个输入更改。 Only first load debounce
works.只有第一次加载debounce
有效。
https://codesandbox.io/s/debounce-not-working-j4ixgg?file=/src/App.js 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>
);
}
In your sandbox, loadData
was being successfully debounced, however it was always executed with inputValue === ""
which then matched all of the options.在您的沙箱中, loadData
已成功去抖动,但它始终使用inputValue === ""
执行,然后匹配所有选项。 All of the actual filtering was then being immediately done by the Autocomplete
component just the same as if you had provided the full set of options statically.所有实际过滤随后都由Autocomplete
组件立即完成,就好像您静态提供了完整的选项集一样。 Due to the behavior of JavaScript closures, inputValue
is always the empty string because that is its initial value as you create the debounced version of loadData
.由于 JavaScript 闭包的行为, inputValue
始终为空字符串,因为这是您创建去抖动版本的loadData
时的初始值。
Whenever creating debounced functions, I recommend declaring and defining the original function and the debounced version at the top level rather than inside your component.每当创建去抖动函数时,我建议在顶层而不是在组件内部声明和定义原始 function 和去抖动版本。 Any local variables and state that your debounced function is dependent on can be passed in as arguments to the function (eg in my version of your code, the input value and the setOptions
function are passed to debouncedLoadData
).您的 debounced function 所依赖的任何局部变量和 state 都可以作为 arguments 传递给 function(例如,在我的代码版本中,输入值和setOptions
function 传递给debouncedLoadData
)。 This avoids accidentally using stale values due to closure behavior and removes the need for useCallback
.这避免了由于关闭行为而意外使用过时的值,并消除了对useCallback
的需要。
Below is a reworked version of your code that moves loadData
and debouncedLoadData
to the top level.下面是将loadData
和debouncedLoadData
移动到顶层的代码的修改版本。 I also changed loadData
to provide no data (rather than all data) when it is passed an empty string so that the debounce behavior is easier to see -- otherwise whenever the Autocomplete has the full set of options, the filtering will occur immediately based on the options that the Autocomplete already has.我还更改了loadData
以在传递空字符串时不提供任何数据(而不是所有数据),以便更容易看到去抖动行为——否则只要自动完成具有完整的选项集,过滤就会立即发生基于自动完成已有的选项。 This also more closely simulates what real code would likely do (ie avoid calling the back end for empty string).这也更接近于模拟真实代码可能执行的操作(即避免为空字符串调用后端)。
import { useState } from "react";
import { Autocomplete, TextField } from "@mui/material";
import { debounce } from "lodash";
import topFilms from "./topFilms";
const loadData = (inputValue, setOptions) => {
if (inputValue.length === 0) {
console.log("no options");
setOptions([]);
return;
}
const filteredOptions = topFilms.filter((f) =>
f.title.toLowerCase().includes(inputValue.toLowerCase())
);
console.log(
`loadData with ${filteredOptions.length} options based on "${inputValue}"`
);
setOptions(filteredOptions);
};
const debouncedLoadData = debounce(loadData, 1000);
export default function App() {
const [value, setValue] = useState(null);
const [inputValue, setInputValue] = useState("");
const [options, setOptions] = useState([]);
const handleInputChange = (e, v) => {
setInputValue(v);
debouncedLoadData(v, setOptions);
};
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>
);
}
tldr: codesandbox link tldr:codesandbox 链接
You can accomplish this with a few hooks working together.你可以通过几个钩子一起工作来完成这个。 First lets look at a hook for debouncing state:首先让我们看一下用于去抖动 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. useEffect 末尾的回调阻止了一堆计时器一个接一个地触发。
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.这里的useEffect是在依赖debouncedValue
通过react state业务改变时运行的。 When typing in the TextField your console should look like:在 TextField 中键入时,您的控制台应如下所示:
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. App
中的这个useEffect
为您提供了一个很好的位置,可以像您提到的那样获取到服务器。 The useEffect
could easily be replaced with something like useQuery useEffect
可以很容易地替换为useQuery 之类的东西
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.