useEffect与去抖动

时间:2019-08-18 19:07:54

标签: javascript reactjs react-hooks

我正在尝试创建一个其值反跳的输入字段(以避免不必要的服务器故障)。 第一次渲染组件时,我会从服务器获取其值(存在加载状态以及所有状态)。

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

这是我的防抖钩:

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>
  );
}

当我运行这段代码时,我希望它忽略第一次的去抖动值更改(仅),因此它应该显示提交次数为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 个答案:

答案 0 :(得分:1)

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

首先,请勿使用useEffect将props.title复制到ExampleTitleInput中的本地状态,因为这可能会导致过多的重新渲染(首先是更改道具,而不是将状态更改为副作用) )。直接使用props.title并将去抖/状态管理部分移至父组件。您只需要传递一个onChange回调作为道具(考虑使用useCallback)。

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

如果您不希望它在第一个渲染中触发,则可以使用来自useUpdateEffecthttps://github.com/streamich/react-use/blob/master/src/useUpdateEffect.ts的自定义钩子,例如react-use,该钩子已经实现了{{ 1}}相关逻辑。

答案 1 :(得分:1)

检查我们是否在第一个渲染中的一种简单方法是设置一个在循环结束时更改的变量。您可以使用组件内部的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]);

    // ...

答案 2 :(得分:1)

您可以更改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;
}