【问题标题】:useEffect with react-hooks/exhaustive-deps where callbacks depend on stateuseEffect 与 react-hooks/exhaustive-deps 其中回调取决于状态
【发布时间】:2021-06-24 16:39:14
【问题描述】:

我遇到了一个无法找到答案的钩子问题。最接近的文章是this other post

本质上,我希望有一个函数,它在每个组件生命周期中使用钩子调用一次。我通常会这样使用useEffect

useEffect(() => {
  doSomethingOnce()  
  setInterval(doSomethingOften, 1000)
}, [])

我正在尝试不禁用 eslint 规则并收到 react-hooks/exhaustive-deps 警告。

将此代码更改为以下代码会产生一个问题,即每次呈现组件时都会调用我的函数...我不想要这个。

useEffect(() => {
  doSomethingOnce()
  setInterval(doSomethingOften, 1000)
}, [doSomethingOnce])

建议的解决方案是将我的函数包装在 useCallback 中,但我找不到答案是如果 doSomethingOnce 取决于状态。

下面是我想要实现的最小代码示例:

import "./styles.css";
import { useEffect, useState, useCallback } from "react";

export default function App() {
  const [count, setCount] = useState(0);

  const getRandomNumber = useCallback(
    () => Math.floor(Math.random() * 1000),
    []
  );

  const startCounterWithARandomNumber = useCallback(() => {
    setCount(getRandomNumber());
  }, [setCount, getRandomNumber]);

  const incrementCounter = useCallback(() => {
    setCount(count + 1);
  }, [count, setCount]);

  useEffect(() => {
    startCounterWithARandomNumber();
    setInterval(incrementCounter, 1000);
  }, [startCounterWithARandomNumber, incrementCounter]);

  return (
    <div className="App">
      <h1>Counter {count}</h1>
    </div>
  );
}

正如您从this demo 看到的那样,因为incrementCounter 依赖于count,它会被重新创建。这反过来又重新调用了我只想被调用一次的useEffect 回调。结果是startCounterWithARandomNumberincrementCounter 被调用的次数比我预期的要多。

任何帮助将不胜感激。

更新

我应该指出,这是一个实际用例的最小示例,其中事件是异步的。

在我的真实代码中,上下文是一个实时转录应用程序。我最初对 GET /api/all.json 进行 fetch 调用以获取整个转录,然后每秒轮询 GET /api/latest.json,将最新的语音与当前状态的文本合并。我试图在我的最小示例中模拟这一点,种子开始,然后是一个依赖于当前状态的轮询方法调用。

【问题讨论】:

    标签: javascript reactjs react-hooks


    【解决方案1】:

    我认为你的代码过于复杂了。

    解决方案

    如果您希望startCounterWithARandomNumber 函数运行一次以设置初始状态,则只需使用状态初始化函数即可。

    const getRandomNumber = () => Math.floor(Math.random() * 1000);
    const [count, setCount] = useState(getRandomNumber);
    

    至于设置间隔的效果,您也希望在安装时只运行一次。将“滴答”的间隔回调移动并增加count 状态进入效果回调,使其不再是依赖项。使用功能状态更新从先前状态而不是初始状态正确更新。不要忘记从useEffect 挂钩返回一个清理函数来清除所有运行的间隔计时器。

    useEffect(() => {
      const incrementCounter = () => setCount((c) => c + 1);
      const timer = setInterval(incrementCounter, 1000);
      return () => clearInterval(timer);
    }, []);
    

    演示

    完整代码:

    import { useEffect, useState } from "react";
    
    export default function App() {
      const getRandomNumber = () => Math.floor(Math.random() * 1000);
      const [count, setCount] = useState(getRandomNumber);
    
      useEffect(() => {
        const incrementCounter = () => setCount((c) => c + 1);
        const timer = setInterval(incrementCounter, 1000);
        return () => clearInterval(timer);
      }, []);
    
      return (
        <div className="App">
          <h1>Counter {count}</h1>
        </div>
      );
    }
    

    更新

    我想你的真实代码和用例在做什么仍然有点不清楚。看来您已经编写了 doSomethingOncedoSomethingOften 函数,以便仍然具有 一些 外部依赖项,当在 useEffect 挂钩中使用时,linter 会对其进行标记。

    根据您在此处的计数示例,该示例不会对依赖项发出警告。

    const getRandomNumber = () => Math.floor(Math.random() * 1000);
    
    const fetch = () =>
      new Promise((resolve) => {
        setTimeout(() => {
          if (Math.random() < 0.1) { // 10% to return new value
            resolve(getRandomNumber());
          }
        }, 3000);
      });
    
    function App() {
      const [count, setCount] = React.useState(0);
    
      const doSomethingOnce = () => {
        console.log('doSomethingOnce');
        fetch().then((val) => setCount(val));
      };
      
      const doSomethingOften = () => {
        console.log('doSomethingOften');
        fetch().then((val) => {
          console.log('new value, update state')
          setCount(val);
        });
      };
    
      React.useEffect(() => {
        doSomethingOnce();
        const timer = setInterval(doSomethingOften, 1000);
    
        return () => clearInterval(timer);
      }, []);
    
      // This effect is only to update state independently of any state updates from "polling"
      React.useEffect(() => {
        const tick = () => setCount((c) => c + 1);
        const timer = setInterval(tick, 100);
        return () => clearInterval(timer);
      }, []);
    
      return (
        <div className="App">
          <h1>Counter {count}</h1>
        </div>
      );
    }
    
    const rootElement = document.getElementById("root");
    ReactDOM.render(
      <App />,
      rootElement
    );
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
    <div id="root"></div>

    这是一种人为和“调整”的解决方案,因为“获取”仅在返回新值时才解决。实际上,如果您可能会轮询并完全替换组件定期修改的一块状态(至少我希望它不会因为这会导致合并/同步更困难)。如果您的代码实际在做什么有更好/更清晰的视图,我可以改进这个答案。

    【讨论】:

    • 感谢您的帮助@drew-reese。我试图创建一个异步的真实用例的最小示例。在我的真实代码中,上下文是一个实时转录应用程序。我最初调用GET /api/all.json 获取整个转录,然后每秒轮询GET /api/latest.json,将最新的语音与当前状态的文本合并。我试图在我的最小示例中模拟这一点,种子开始,然后是一个轮询方法调用,该方法调用依赖于当前状态。
    • @adambutler 啊,我明白了,我想你当时有点错过了目标。您能否更新您的问题以包含更具代表性的代码示例?
    • 我已经更新了我的回复和原始帖子以澄清它。感谢您的帮助。
    • @adambutler 用例仍然有点不清楚,但我已经根据您的计数示例更新了我的答案以包括异步调用。我的猜测是您已经编写了 doSomethingOncedoSomethingOften 函数以具有外部依赖关系,并且这由 linter 标记。
    【解决方案2】:

    我不知道您要解决的用例,因此请对您的代码进行最少的代码更改。 一个非常简单的解决方案是将计数器递增为

    const incrementCounter = useCallback(() => {
        setCount(prevCount => prevCount+1);
    }, [setCount]);
    

    这样增量计数器不依赖于计数。

    从您的沙盒中分叉。 https://codesandbox.io/s/fragrant-architecture-t58bs

    【讨论】:

      猜你喜欢
      • 2020-06-08
      • 1970-01-01
      • 2021-06-10
      • 2020-01-18
      • 2020-05-01
      • 2021-04-15
      • 2020-06-22
      • 2020-02-21
      • 2020-06-09
      相关资源
      最近更新 更多