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. theuseEffect
calls thesetState
in the parent, which creates a new function, which reruns theuseEffect
. – 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
4 Answers
Reset to default 2How 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
版权声明:本文标题:javascript - React useState with an empty object causes an infinite loop - Stack Overflow 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.betaflare.com/web/1742003531a2411499.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论