用空对象反应useState导致无限循环

时间:2020-10-13 22:32:05

标签: javascript reactjs react-hooks infinite-loop

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

3 个答案:

答案 0 :(得分:2)

如何将状态 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>

答案 1 :(得分:2)

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

在您useEffect中,您正在比较2个对象,因为它们始终具有不同的引用,因此在JS领域中它们永远不会相同,并且您的useEffect将触发并设置新对象,自己陷入无限循环。

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

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

这样,您会将原始值传递给子组件,useEffect的行为将更加可预测。

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

答案 2 :(得分:1)

关于解决方案,我建议您不要在子组件中设置任何初始状态(或将其设置为空对象{})。第一个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>;
};

我同意其他评论,而不是将州由父母传给孩子。