简体   繁体   English

去抖动 MUI 自动完成功能无法正常工作

[英]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.下面是将loadDatadebouncedLoadData移动到顶层的代码的修改版本。 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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM