【问题标题】:React useState with an empty object causes an infinite loop使用空对象反应 useState 会导致无限循环
【发布时间】:2021-01-28 07:33:05
【问题描述】:

将 React 钩子与子组件一起使用,该子组件应从父组件获取初始状态,并在每次内部状态更改时更新父组件。
我认为,由于它始终是相同的引用,因此不应无限调用子级的 useEffect。

如果孩子的初始状态是一个空对象,我会得到一个无限循环。
如果孩子的初始状态取自道具,效果很好。

不确定是什么原因造成的。
您可以将子组件内部的第一个 useState 更改为空对象,以启动无限循环。

请查看下面的沙盒:
https://codesandbox.io/s/weird-initial-state-xi5iy?fontsize=14&hidenavigation=1&theme=dark
注意:我在沙箱中添加了一个计数器,以在 10 次运行后停止循环,而不会使浏览器崩溃。

import React, { useState, useEffect, useCallback } from "react";

const problematicInitialState = {};

/* CHILD COMPONENT */
const Child = ({ onChange, initialData }) => {
  const [data, setData] = useState(initialData); // if initialData is {} (a.k.a problematicInitialState const) we have an infinite loop

  useEffect(() => {
    setData(initialData);
  }, [initialData]);

  useEffect(() => {
    onChange(data);
  }, [data, onChange]);

  return <div>Counter is: {data.counter}</div>;
};

/* PARENT COMPONENT */
export default function App() {
  const [counterData, setCounterData] = useState({ counter: 4 });

  const onChildChange = useCallback(
    (data) => {
      setCounterData(data);
    },
    [setCounterData]
  );

  return (
    <div className="App">
      <Child onChange={onChildChange} initialData={counterData} />
    </div>
  );
}

【问题讨论】:

  • 如果不深入研究,我认为这是由第二个 useEffect [data, onChange] 的依赖数组引起的。每次父组件重新渲染时,子组件中的 onChange 将是一个新的参考。由于any call to setState 导致重新渲染,您创建了一个循环,即useEffect 调用父级中的setState,这将创建一个新函数,该函数重新运行useEffect
  • 感谢@JacobSmit 的输入。根据您的评论,我已将 useCallback 添加到我的问题的代码 sn-p 中(尽管我在沙箱中使用了 useCallback)。然而,这并不能解决问题:(
  • onChange 是一个明显的问题,但不是唯一的问题。我没有机会调试,但我想问题是不同的初始子状态使两个具有不同输入的 useEffect 争夺状态并在 {} 和 {counter:4} 之间无限交换。他们应该以某种方式返工。初始状态和当前状态之间应该有明显的区别,并且应该有一个单一的事实来源。如果您不想像答案建议的那样将状态提升给父母,至少不要从孩子那里更新父母的初始状态。父母甚至需要知道孩子的当前状态吗?
  • 感谢@EstusFlask。我不会更新父母的初始状态,但是当孩子的状态发生变化时会经常更新它。在我的真实项目中,孩子需要更新父级,以便它可以更新不同的兄弟姐妹,这就是为什么我将更改传递给父级,以便他可以将其传递给另一个兄弟姐妹。
  • 嗯,你这样做是因为当你更新父状态时,你更新了初始数据。根据你的描述,这是一个完全有效的提升国家的案例。如果有 3 个孩子,那么父母将处理 3 个状态。

标签: javascript reactjs react-hooks infinite-loop


【解决方案1】:

将状态 only 放在父组件中如何,让子组件仅引用传递给它的道具,而没有任何自己的状态?

const Child = ({ counterData, setCounterData }) => {
  return (
    <div>
      <div>Counter is: {counterData.counter}</div>
      <button
        onClick={() => setCounterData({ counter: counterData.counter + 1 })}
      >increment</button>
    </div>
  );
};

const App = () => {
  const [counterData, setCounterData] = React.useState({ counter: 4 });
  return (
    <div className="App">
      <Child {...{ counterData, setCounterData }} />
    </div>
  );
}

ReactDOM.render(<App />, document.querySelector('.react'));
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<div class="react"></div>

【讨论】:

  • 谢谢。我知道这是一个可能的解决方案。因为这只是我拥有的一个真实项目的一个例子,它有 3 个子组件,我宁愿不把所有 3 个组件的状态都放在父组件中。我真的不明白这个无限循环的根本原因。
【解决方案2】:

问题是在 JS {} !== {} 中,因为对象与原始值不同,是通过引用而不是值进行比较的。

在你useEffect 中,你在比较 2 个对象,因为它们总是有不同的引用,在 JS 领域永远不会相同,你的 useEffect 将触发,设置新对象,你得到一个无限循环.

你不应该像在 react 中使用类组件那样使用钩子,这意味着你应该这样做

const [counter, setCounter] = useState(4);

这样,您将原始值向下传递给您的子组件,useEffect 将有更多可预测的行为。

此外,虽然这是一个测试用例,但您应该很少(阅读:从不)尝试将子状态设置为父状态。您已经将该数据从父组件传递给子组件,无需在子组件中创建冗余状态,只需使用传入的数据即可。

【讨论】:

  • 感谢您的意见。顺便说一句,我尝试在子功能组件之外声明一个 const 并将其传递给 useState 而不是仅使用 {} 但它没有解决问题......
  • 这肯定会导致问题,但我怀疑在这种情况下它也会导致无限循环(父级中的 useState(4) 和子级中的 useState(undefined))。
  • 这不是因为你还在做同样的事情,只是声明不同。您仍然在每次渲染时用新对象替换对象并进入无限循环。 Hooks 与 class 组件中的 state 不同,使用 hooks 替换之前调用 setState 时的状态,使用 class 组件扩展它
  • @Loves2Develop 即使父母和孩子的 useState({ counter:4}) 相同,它的行为也会如此。问题不在于每次渲染时 {} 不同(useState 只考虑一次),而是父子对象中的对象引用不同,即使计数器属性值相同。
【解决方案3】:

关于解决方案,我建议您不要在子组件中设置任何初始状态(或将其设置为空对象{})。第一个useEffect 将处理第一个更新。

const Child = ({ onChange, initialData }) => {
  const [data, setData] = useState({});

  useEffect(() => {
    setData(initialData);
  }, [initialData]);

  useEffect(() => {
    onChange(data);
  }, [data, onChange]);

  return <div>Counter is: {data.counter}</div>;
};

对于其他 cmets,我同意,而是将状态从父级传递给子级。

【讨论】:

    猜你喜欢
    • 2020-03-25
    • 2021-11-11
    • 1970-01-01
    • 1970-01-01
    • 2021-04-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-10-27
    相关资源
    最近更新 更多