Using React Version: 16.8.4
When using blank object or array as a setter for function inside useEffect does cause infinite loop. However using string or number does not trigger this. Here's the minimal example:
function Example() {
const [count, setCount] = useState(0);
const [foo, setFoo] = useState({});
useEffect(() => {
console.log("Infinite Loop");
setCount(100); // doesn't cause infinite loop.
// but this does
// setFoo({}); // or setFoo([]);
});
return <div />;
}
I don't know if it's a bug or default behaviour because in earlier version of react (in v16.7.0-alpha.0), both string and number also causing infinite loop which seems to be fixed in latest one.
It seems to behave according to the docs.
If you update a State Hook to the same value as the current state, React will bail out without rendering the children or firing effects.
Both {} and [] creates a new object or array and hence triggers an update. 100 is primitive and doesn't.
Hmm, this is actually the case, but IMO react should not create new object every time if it doesn't change (which causing infinite loop). Otherwise we don't have a way to update state with object without adding dependency array. Consider this real world example:
function Example() {
const [currentUser, setCurrentUser] = useState({});
useEffect(() => {
const unsub = firebase.auth().onAuthStateChanged(user => {
unsub();
if(user) setCurrentUser(user);
});
})
return <div/>
}
Now we don't have a way to avoid infinite loop in this case without adding setCurrentUser or currentUser.uid in dependency array.
You can avoid your infinite loops yourself by using more conditions before calling setCurrentUser, .e.g.
const unsub = firebase.auth().onAuthStateChanged(user => {
unsub();
// Do nothing if the user hasn't changed.
if (user && currentUser && user.username === currentUser.username) {
return;
})
setCurrentUser(user);
});
I haven't used firebase myself but by the looks of it you you may be able to do something like this:
function Example() {
const [currentUser, setCurrentUser] = useState({});
useEffect(() => {
const unsub = firebase.auth().onAuthStateChanged(user => {
// Do nothing if the user hasn't changed.
if (user && currentUser && user.username === currentUser.username) {
return;
}
setCurrentUser(user);
});
// Unsub when the componenent unmounts.
return () => unsub();
}, []); // Empty array so this effect only is called on mount and unmount.
return <div/>;
}
passing condition in dependency array and checking explicitly is the same thing.
I believe it is the default behaviour. React does shallow comparing. Passing a {} will fail the shallow compare.
I believe the best strategy is to pass a dependency if there is a dependency on when the effect should run.
There is these fresh articles I highly recommend to read
However, when you run your application, you should stumble into a nasty loop .... Because we are setting the state after every data fetch, the component updates and the effect runs again. It fetches the data again and again. That’s a bug and needs to be avoided
https://www.robinwieruch.de/react-hooks-fetch-data/ - by Robin Wieruch
https://overreacted.io/a-complete-guide-to-useeffect/ - by Abramov
Re: example in https://github.com/facebook/react/issues/15096#issuecomment-472349151, you can fix it by adding the dependencies argument. (In this case, it's [] because your effect doesn't depend on any values from render scope except setCurrentUser which is guaranteed to be stable.)
function Example() {
const [currentUser, setCurrentUser] = useState({});
useEffect(() => {
const unsub = firebase.auth().onAuthStateChanged(user => {
unsub();
if(user) setCurrentUser(user);
});
}, []) // <--------- I added this
return <div/>
}
Note that you should only do it when it's safe — i.e. when you don't use any props/state and values derived from them in the effect, and you also don't call any functions that use those.
See:
Hope this helps.
For a deeper dive: