简体   繁体   English

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

[英]React useState with an empty object causes an infinite loop

Using React hooks with a child component that should get the initial state from the parent and update the parent on every internal state change.将 React 钩子与子组件一起使用,该子组件应该从父组件获取初始状态并在每次内部状态更改时更新父组件。
I figured that since it's always the same reference the useEffect of the child should not get called infinitely.我认为由于它始终是相同的引用,因此不应无限调用 child 的 useEffect。

If the initial state of the child is an empty object I get an infinite loop.如果孩子的初始状态是一个空对象,我会得到一个无限循环。
If the initial state of the child is taken from the props it works great.如果孩子的初始状态是从道具中获取的,那么效果很好。

Not sure what's causing it.不确定是什么原因造成的。
You can change the first useState inside the child component to an empty object to make the infinite loop start.您可以将子组件内的第一个 useState 更改为空对象,以启动无限循环。

Please review the sandbox below:请查看下面的沙箱:
https://codesandbox.io/s/weird-initial-state-xi5iy?fontsize=14&hidenavigation=1&theme=dark https://codesandbox.io/s/weird-initial-state-xi5iy?fontsize=14&hidenavigation=1&theme=dark
Note: I've added a counter to the sandbox to stop the loop after 10 runs and not crash the browser.注意:我在沙箱中添加了一个计数器,以在运行 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>
  );
}

How about putting the state only in the parent component instead, and have the child only reference the props passed down to it, without any state of its own?如何将状态放在父组件中,而让子组件只引用传递给它的道具,而没有任何自己的状态?

 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>

Problem is that in JS {} !== {} because objects, unlike primitive values, are compared by reference, not value.问题是在 JS {} !== {}因为对象与原始值不同,是通过引用而不是值进行比较。

In you useEffect you're comparing 2 objects, because they always have different reference, the'll never be the same in JS land and your useEffect will trigger, setting new object and you got yourself an infinite loop.在你useEffect你正在比较 2 个对象,因为它们总是有不同的引用,在 JS 领域永远不会相同,你的useEffect将触发,设置新对象,你得到了一个无限循环。

You shouldn't use hooks in the same way you used class components in react, meaning you should do您不应该像在 React 中使用类组件那样使用钩子,这意味着您应该这样做

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

This way, you'll pass primitive value down to your child component and useEffect will have much more predictable behaviour.这样,您将原始值传递给您的子组件,并且useEffect将具有更可预测的行为。

Also, while this is a test case, you should rarely (read: never) try to set child sate to parent state.此外,虽然这是一个测试用例,但您应该很少(阅读:从不)尝试将子状态设置为父状态。 You already pass that data from parent to child, no need to create redundant state in your child component, just use the passed in data.您已经将数据从父组件传递给子组件,无需在子组件中创建冗余状态,只需使用传入的数据即可。

Regarding solutions I propose that you don't set any initial state (or set it as empty object {} ) in your child component.关于解决方案,我建议您不要在子组件中设置任何初始状态(或将其设置为空对象{} )。 The first useEffect will handle the first update.第一个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>;
};

as of the other comments, I agree, rather pass the state from parent to child.至于其他评论,我同意,而是将状态从父母传递给孩子。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM