useEffect 带有 react-hooks/exhaustive-deps,其中回调取决于状态

时间:2021-06-24 16:39:14

标签: javascript reactjs react-hooks

我遇到了一个无法找到答案的钩子问题。最接近的文章是 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,将最新的语音与当前状态的文本合并。我尝试在我的最小示例中模拟这一点,使用一个种子开始,然后是一个依赖于当前状态的轮询方法调用。

2 个答案:

答案 0 :(得分:0)

我认为您的代码过于复杂。

解决方案

如果您希望 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);
}, []);

演示

Edit useeffect-with-react-hooks-exhaustive-deps-where-callbacks-depend-on-state

完整代码:

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 函数,以便仍然具有 some 外部依赖,当在 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>

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

答案 1 :(得分:0)

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

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

这种方式增量计数器不依赖于计数。

从你的沙箱中分叉出来。 https://codesandbox.io/s/fragrant-architecture-t58bs