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.
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 component 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>
);
}
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.
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.
You shouldn't use hooks in the same way you used class components in react, meaning you should do
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With