Reputation: 4232
I created a project with create-react-app,
and I am trying React hooks,
in below example,
the sentence console.log(articles)
runs endlessly:
import React, {useState, useEffect} from "react"
import {InfiniteScroller} from "react-iscroller";
import axios from "axios";
import './index.less';
function ArticleList() {
const [articles, setArticles] = useState([]);
useEffect(() => {
getArticleList().then(res => {
setArticles(res.data.article_list);
console.log(articles);
});
},[articles]);
const getArticleList = params => {
return axios.get('/api/articles', params).then(res => {
return res.data
}, err => {
return Promise.reject(err);
}).catch((error) => {
return Promise.reject(error);
});
};
let renderCell = (item, index) => {
return (
<li key={index} style={{listStyle: "none"}}>
<div>
<span style={{color: "red"}}>{index}</span>
{item.content}
</div>
{item.image ? <img src={item.image}/> : null}
</li>
);
};
let onEnd = () => {
//...
};
return (
<InfiniteScroller
itemAverageHeight={66}
containerHeight={window.innerHeight}
items={articles}
itemKey="id"
onRenderCell={renderCell}
onEnd={onEnd}
/>
);
}
export default ArticleList;
Why is it?How to handle it?
Upvotes: 4
Views: 2928
Reputation: 528
How to make it stop running infinitely:
Replace your useEffect
hook with:
// ...
useEffect(() => {
getArticleList().then(res => {
setArticles(res.data.article_list);
console.log(articles);
});
},[JSON.stringify(articles)]); // <<< addition
// ...
Explanation:
Before we understand why it runs infinitely or why we did that addition to stop it, we must understand 3 things:
const arr1 = [];
const arr2 = [];
console.log(arr1 === arr2) // false
arr1
and arr2
are not equal because their references are different (they're 2 different instances). The same would be true if each was equal to {}
, because those are also objects.Why the hook is running infinitely:
articles
, which is a part of the dependency array, seems to get created (in res.data.article_list
) and set to articles
(through setArticles
). This new instance is not equal to the older one because [remember] JavaScript compares objects based on reference not value. This causes the hook to re-run infinitely.Why using JSON.stringify()
stops it from running infinitely:
JSON.stringify()
converts to a string, which is a primitive type, and is compared based on value (not reference). If the content of res.data.article_list
is the same, then React compares the old stringified version of it with the new one, sees that they're the same, and doesn't re-run the hook.JSON.stringify(arr)
in the dependency array is better than arr.length
:Doing arr.length
returns a number which is a primitive value. JS doesn't compare primitive values by reference, it compares them by value. I.e. 0 === 0 // true
, so React won't re-run it if arr.length
doesn't change, , even if the array content did change.
So if we have articles = ['earth is melting']
and then we set a new value to articles[0]
like articles = ['earth is healing']
. It will think it's the same and will not re-run the hook, because both articles.length
s evaluate to 1
.
['earth is melting'].length === ['earth is healing'].length // true
Using JSON.stringify(arr)
, on the other hand, converts the array to a string (e.g. '[]'
).
"['earth is melting']" === "['earth is healing']" // false
So JSON.stringify(arr)
will re-run useEffect
every time the actual content OR length of the array changes. Meaning that it's more specific.
Upvotes: 0
Reputation: 76
This is a gotcha with the React's useEffect hook.
This kind of similar to how Redux reducers work. Every time an object is passed in as the second argument inside of a useEffect array, it is considered a different object in memory.
For example if you call
useEffect(() > {}, [{articleName: 'Hello World'}])
and then a re-render happens, it will be called again every time. So passing in the length of the articles is a way to bypass this. If you never want the function to be called a second time, you can pass in an empty array as an argument.
Upvotes: 2
Reputation: 41
I ran into this with an array objects. I found just passing [array.length] as the second argument did not work, so I had to create a second bit of state to keep up with array.length and call setArrayLength when manipulating the array.
const = [array, setArray] = useState([]);
const = [arrayLength, setArrayLength] = useState(array.length)
useEffect(() => {
const fetchData = async () => {
const result = await axios.get(// whatever you need);
setArray(result.data)
};
fetchData();
}, [arrayLength]);
const handleArray () => {
// Do something to array
setArrayLength(array.length);
}
There is a deep comparison utilizing useRef that you can see the code here. There is an accompanying video on egghead, but it's behind a paywall.
Upvotes: 0
Reputation: 21
It is simply because you cannot compare two array (which are simply) objects in JavaScript using ===. Here what you are basically doing is comparing the previous value of articleName to current value of articleName, which will always return false. Since
That comparison by reference basically checks to see if the objects given refer to the same location in memory.
Here its not, so each time re-rendering occurs. The solution is to pass a constant like array length or if you want a tight check then create a hash from all element in the array and compare them on each render.
Upvotes: 2
Reputation: 116
React useEffect compares the second argument with it previous value, articles in your case. But result of comparing objects in javascript is always false, try to compare [] === [] in your browser console you will get false. So the solution is to compare not the whole object, but articles.lenght
const [articles, setArticles] = useState([]);
useEffect(() => {
getArticleList().then(res => {
setArticles(res.data.article_list);
console.log(articles);
});
},[articles.length]);
Upvotes: 6