Reputation: 2392
Seeing some weird things with a hook we use for dealing with apiCalls, where a sudden change in api response structure triggered infinite request loops in our app. I have slimmed it down to the bare requirements to reproduce the bug.
import React, {useEffect, useState, useMemo} from 'react';
const mockedApiCall = (resolvedValue, time) =>
new Promise((resolve, reject) => setTimeout(() => resolve(resolvedValue), time));
const useHook = (query) => {
const [error, setError] = useState(null);
const [data, setData] = useState([]);
useEffect(() => {
const asyncOperation = async () => {
const response = await mockedApiCall([], 1000);
// Testing something i know will fail:
const mappedData = response.data.map(a => a);
// if the above didn't fail we would update data
setData(mappedData);
};
// Clear old errors
setError(null);
// trigger operation
asyncOperation().catch(e => {
console.log('fail');
setError('Some error');
});
}, [query]);
return [data, error];
}
const HookIssue = props => {
const [data, error] = useHook({foo: 'bar'}); // Creates infinite loop
//const [data, error] = useHook('foo') // only fails a single time
// Alternative solution that also doesn't create infinite loop
// const query = useMemo(() => ({foo: 'bar'}), []);
// const [data, error] = useHook(query);
return null;
};
export default HookIssue;
Does anyone know what is happening here? The hook is supposed to only listen to changes in the query variable, but in some cases will just run into an infinite loop.
Edit: To add some more weirdness to this issue, if i remove either (or both) of the two setError calls inside the useEffect it would also prevent the infinite loop.
Upvotes: 3
Views: 223
Reputation: 10538
You're getting this loop because you're passing an object to useHook()
. Objects are reference types - that is, they aren't defined by their value. The following statement evaluates to false for this reason:
{ foo: "bar" } === { foo: "bar" }
Every time HookIssue
is rendered, it will create a new object of { foo: "bar" }
. This is passed to useEffect()
in the dependency array, but because objects are reference types - and each object on each render will have a different reference - React will always "see" the query
value as having changed.
This means that every time asyncOperation
finishes, it will cause a re-render of HookIssue
, which will cause another effect to be triggered.
To fix this, you need to pass a value type or a string to the dependency array instead, or you need to "stabilize" the query
object through the use of useMemo()
.
Upvotes: 6