带去抖动的 useEffect

IT技术 javascript reactjs react-hooks
2021-05-19 15:44:52

我正在尝试创建一个输入字段,该字段的值已去抖动(以避免不必要的服务器行程)。第一次渲染我的组件时,我从服务器获取它的值(有一个加载状态等等)。

这是我所拥有的(出于示例的目的,我省略了不相关的代码)。

这是我的去抖钩:

export function useDebounce(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => clearTimeout(handler);
  }, [value, delay]);

  return debouncedValue;
}

(我从:https : //usehooks.com/useDebounce/得到这个

是的,这是我的组件以及我如何使用useDebounce钩子:

function ExampleTitleInput(props) {
  const [title, setTitle] = useState(props.title || "");
  const [lastCommittedTitle, setLastCommittedTitle] = useState(title);
  const [commitsCount, setCommitsCount] = useState(0);

  const debouncedTitle = useDebounce(title, 1000);

  useEffect(() => {
    setTitle(props.title || "");
  }, [props.title]);

  useEffect(() => {
    if (debouncedTitle !== lastCommittedTitle) {
      setLastCommittedTitle(debouncedTitle);
      setCommitsCount(commitsCount + 1);
    }
  }, [debouncedTitle, lastCommittedTitle, commitsCount]);

  return (
    <div className="example-input-container">
      <input
        type="text"
        value={title}
        onChange={e => setTitle(e.target.value)}
      />
      <div>Last Committed Value: {lastCommittedTitle}</div>
      <div>Commits: {commitsCount}</div>
    </div>
  );
}

这是父组件:

function App() {
  const [title, setTitle] = useState("");

  useEffect(() => {
    setTimeout(() => setTitle("This came async from the server"), 2000);
  }, []);

  return (
    <div className="App">
      <h1>Example</h1>
      <ExampleTitleInput title={title} />
    </div>
  );
}

当我运行此代码时,我希望它第一次(仅)忽略 debounce 值更改,因此它应该显示提交次数为 0,因为该值是从 props 传递的。应跟踪任何其他更改。对不起,我度过了漫长的一天,在这一点上我有点困惑(我认为这个“问题”已经很久了)。

我创建了一个示例:

https://codesandbox.io/s/zen-dust-mih5d

它应该显示提交次数为 0 并且正确设置的值没有去抖动更改。

我希望我说得有道理,如果我能提供更多信息,请告诉我。

编辑

这完全符合我的预期,但是它给了我“警告”(注意 deps 数组中缺少依赖项):

function ExampleTitleInput(props) {
  const [title, setTitle] = useState(props.title || "");
  const [lastCommittedTitle, setLastCommittedTitle] = useState(title);
  const [commitsCount, setCommitsCount] = useState(0);

  const debouncedTitle = useDebounce(title, 1000);

  useEffect(() => {
    setTitle(props.title || "");
    // I added this line here
    setLastCommittedTitle(props.title || "");
  }, [props]);

  useEffect(() => {
    if (debouncedTitle !== lastCommittedTitle) {
      setLastCommittedTitle(debouncedTitle);
      setCommitsCount(commitsCount + 1);
    }
  }, [debouncedTitle]); // removed the rest of the dependencies here, but now eslint is complaining and giving me a warning that I use dependencies that are not listed in the deps array

  return (
    <div className="example-input-container">
      <input
        type="text"
        value={title}
        onChange={e => setTitle(e.target.value)}
      />
      <div>Last Committed Value: {lastCommittedTitle}</div>
      <div>Commits: {commitsCount}</div>
    </div>
  );
}

这是:https : //codesandbox.io/s/optimistic-perlman-w8uug

这有效,但我担心警告,感觉好像我做错了什么。

3个回答

检查我们是否处于第一次渲染的一种简单方法是设置一个在循环结束时更改的变量。您可以使用组件内的 ref 来实现这一点:

const myComponent = () => {
    const is_first_render = useRef(true);

    useEffect(() => {
        is_first_render.current = false;
    }, []);

    // ...

您可以将其提取到一个钩子中,然后简单地将其导入到您的组件中:

const useIsFirstRender = () => {
    const is_first_render = useRef(true);

    useEffect(() => {
        is_first_render.current = false;
    }, []);

    return is_first_render.current;
};

然后在您的组件中:

function ExampleTitleInput(props) {
    const [title, setTitle] = useState(props.title || "");
    const [lastCommittedTitle, setLastCommittedTitle] = useState(title);
    const [updatesCount, setUpdatesCount] = useState(0);
    const is_first_render = useIsFirstRender(); // Here

    const debouncedTitle = useDebounce(title, 1000);

    useEffect(() => {
        setTitle(props.title || "");
    }, [props.title]);

    useEffect(() => {
        // I don't want this to trigger when the value is passed by the props (i.e. - when initialized)
        if (is_first_render) { // Here
            return;
        }

        if (debouncedTitle !== lastCommittedTitle) {
            setLastCommittedTitle(debouncedTitle);
            setUpdatesCount(updatesCount + 1);
        }
    }, [debouncedTitle, lastCommittedTitle, updatesCount]);

    // ...

您可以更改useDebounce钩子以了解应该立即设置第一个设置的去抖动值这一事实。useRef非常适合:

export function useDebounce(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value);
  const firstDebounce = useRef(true);

  useEffect(() => {
    if (value && firstDebounce.current) {
      setDebouncedValue(value);
      firstDebounce.current = false;
      return;
    }

    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => clearTimeout(handler);
  }, [value, delay]);

  return debouncedValue;
}

我认为您可以通过某些方式改进您的代码:

首先,不要props.titleExampleTitleInputuseEffect 中复制到本地状态,因为它可能会导致过度的重新渲染(首先是改变props,而不是改变状态作为副作用)。props.title直接使用,把debounce/状态管理部分移到父组件。您只需要将onChange回调作为props传递(考虑使用useCallback)。

为了跟踪旧状态,正确的钩子是useRef (API reference)

如果您不希望它在第一次渲染时触发,您可以使用自定义钩子,例如useUpdateEffect,来自react-usehttps : //github.com/streamich/react-use/blob/master/src/useUpdateEffect.ts,即已经实现了useRef相关的逻辑。