尽管依赖关系没有变化,useEffect 运行无限循环

useEffect runs infinite loop despite no change in dependencies

我有一个使用钩子的函数组件。在 useEffect 挂钩中,我只想从后端获取数据并将结果存储在状态中。然而,尽管将数据变量添加为依赖项,useEffect 仍然会在无限循环中触发 - 即使数据没有更改。如何阻止 useEffect 连续触发?

我试过空数组 hack,它确实可以阻止 useEffect 连续触发,但这不是所需的行为。例如,如果用户保存了新数据,useEffect 应该再次触发以获取更新的数据——我不想模拟 componentDidMount。

const Invoices = () => {
  const [invoiceData, setInvoiceData] = useState([]);

  useEffect(() => {
    const updateInvoiceData = async () => {
      const results = await api.invoice.findData();
      setInvoiceData(results);
    };
    updateInvoiceData();
  }, [invoiceData]);

  return (
    <Table entries={invoiceData} />
  );
};

我希望 useEffect 在初始渲染后触发,并且仅在 invoiceData 更改时再次触发。

useEffect 依赖项数组的工作方式是检查上一个渲染和新渲染的数组中所有项目之间的严格 (===) 等价性。因此,将数组放入您的 useEffect 依赖项数组中非常麻烦,因为与 === 的数组比较检查等价性 通过引用 而不是通过内容

const foo = [1, 2, 3];
const bar = foo;
foo === bar; // true

const foo = [1, 2, 3];
const bar = [1, 2, 3];
foo === bar; // false

在您的效果函数内部,当您执行 setInvoiceData(results) 时,您正在将 invoiceData 更新为一个新数组。即使新数组中的所有项目都完全相同,新数组 invoiceDatareference 已经改变,导致效果的依赖性不同,触发再次发挥作用——无穷无尽。

一个简单的解决方案是简单地从依赖项数组中删除 invoiceData。这样,useEffect 函数的作用基本上类似于 componentDidMount,它会在组件首次渲染时触发一次且仅触发一次。

useEffect(() => {
    const updateInvoiceData = async () => {
      const results = await api.invoice.findData();
      setInvoiceData(results);
    };
    updateInvoiceData();
  }, []);

这种模式很常见(而且很有用),甚至在 the official React Hooks API documentation:

中也提到过

If you want to run an effect and clean it up only once (on mount and unmount), you can pass an empty array ([]) as a second argument. This tells React that your effect doesn’t depend on any values from props or state, so it never needs to re-run. This isn’t handled as a special case — it follows directly from how the dependencies array always works.

发生的事情是,当您更新 invoiceData 时,技术上会更改 invoiceData 的状态,您已通过 useEffect 挂钩观察到该状态,这会导致挂钩运行 再次更新 invoiceData。如果你想useEffect到运行挂载,我怀疑,然后将一个空数组传递给useEffect的第二个参数,它模拟componentDidMount在class组件。然后,您将能够使用 useState 挂钩更新本地 UI 状态。

感谢因出色的 "under the hood" 解释而受到嘲笑;我还发现 Milind 将更新方法与 useEffect 分开的建议特别有效。 我的解决方案(为简洁起见被截断)如下 -

const Invoices = () => {
  const [invoiceData, setInvoiceData] = useState([]);

  useEffect(() => {    
    updateInvoiceData();
  }, []);

  // Extracting this method made it accessible for context/prop-drilling
  const updateInvoiceData = async () => {
    const results = await api.invoice.findData();
    setInvoiceData(results);
  };

  return (
    <div>
      <OtherComponentThatUpdatesData handleUpdateState={updateInvoiceData} />
      <Table entries={invoiceData} />
    </div>
  );
};

我完全同意 Jared 的回答。 但是对于一些你真的不想有参考比较的场景,那么使用react-use库中的DeepCompareEffect真的很好 https://github.com/streamich/react-use/blob/HEAD/docs/useDeepCompareEffect.md