admin管理员组

文章数量:1316363

Using React hooks with a child ponent that should get the initial state from the parent and update the parent on every internal state change.
I figured that since it's always the same reference the useEffect of the child should not get called infinitely.

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 ponent to an empty object to make the infinite loop start.

Please review the sandbox below:
;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.

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

Using React hooks with a child ponent that should get the initial state from the parent and update the parent on every internal state change.
I figured that since it's always the same reference the useEffect of the child should not get called infinitely.

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 ponent to an empty object to make the infinite loop start.

Please review the sandbox below:
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.

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>
  );
}
Share Improve this question edited Oct 13, 2020 at 23:38 Loves2Develop asked Oct 13, 2020 at 22:32 Loves2DevelopLoves2Develop 8341 gold badge9 silver badges29 bronze badges 5
  • 2 Without looking into this to deep, I assume this is caused by the dependency array for the second useEffect [data, onChange]. onChange in your child ponent will be a new reference everytime the parent rerenders. As any call to setState causes a rerender you have created a loop, i.e. the useEffect calls the setState in the parent, which creates a new function, which reruns the useEffect. – Jacob Smit Commented Oct 13, 2020 at 22:42
  • Thanks for the input @JacobSmit. Following your ment I've added the useCallback to the code snippet of my question (although I used useCallback in the sandbox). Nevertheless, that doesn't solve the issue :( – Loves2Develop Commented Oct 13, 2020 at 22:50
  • onChange was the obvious problem but not the only one. I didn’t have a chance to debug but I guess the problem is that different initial child state makes two useEffects with different inputs fight for the state and infinitely swap between {} and {counter:4}. They should be reworked somehow. There should be a clear distinction between initial and current state, and there should be a single source of truth. If you don’t want to lift the state to a parent like the answer suggests, at least don’t update parent’s initial state from a child. Does a parent even need to know children current state? – Estus Flask Commented Oct 13, 2020 at 23:39
  • Thanks @EstusFlask. I'm not updating the initial state of the parent, but do update it frequently when the child's state changes. In my real project the child need to update the parent so it can update a different sibling and that's why I pass the changes to the parent so he can pass it to the other sibling. – Loves2Develop Commented Oct 13, 2020 at 23:50
  • Well, you do because when you update parent state, you update initial data. From what you describe, this is a totally valid case to lift up the state. If there are 3 children then a parent will handle 3 states. – Estus Flask Commented Oct 13, 2020 at 23:56
Add a ment  | 

4 Answers 4

Reset to default 2

How about putting the state only in the parent ponent 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./react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg./react-dom@16/umd/react-dom.development.js"></script>
<div class="react"></div>

Problem is that in JS {} !== {} because objects, unlike primitive values, are pared by reference, not value.

In you useEffect you're paring 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.

You shouldn't use hooks in the same way you used class ponents in react, meaning you should do

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

This way, you'll pass primitive value down to your child ponent and useEffect will have much more predictable behaviour.

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 ponent, 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 ponent. The first useEffect will handle the first update.

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 ments, I agree, rather pass the state from parent to child.

If the useEffect Method with an empty object as a dependency causes an infinite loop, then you should wrap the object with JSON.stringify(data). This recently happended to me and fixed everything:

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

本文标签: javascriptReact useState with an empty object causes an infinite loopStack Overflow