'Why React hook useEffect runs endlessly?
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?
Solution 1:[1]
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]);
Solution 2:[2]
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.
Solution 3:[3]
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.
Solution 4:[4]
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.
Solution 5:[5]
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:
- Arrays are JavaScript objects.
- JavaScript compares objects based on reference, not value. Meaning:
const arr1 = [];
const arr2 = [];
console.log(arr1 === arr2) // false
arr1andarr2are 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.
- React re-runs hooks when a value in the dependency array changes. It does a comparison of the old value and the newly set value.
Why the hook is running infinitely:
- [remember] React re-runs the hook when an item in the dependency array changes. A new instance of
articles, which is a part of the dependency array, seems to get created (inres.data.article_list) and set toarticles(throughsetArticles). 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:
- Using
JSON.stringify()converts to a string, which is a primitive type, and is compared based on value (not reference). If the content ofres.data.article_listis 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.
Extra: Why using 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.lengths 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.
Solution 6:[6]
One could also try to configure pandoc to do that for us. Here's what the manual has to say about the --shift-heading-level-by option:
--shift-heading-level-by=NUMBERShift heading levels by a positive or negative integer. For example, with
--shift-heading-level-by=-1, level 2 headings become level 1 headings, and level 3 headings become level 2 headings. Headings cannot have a level less than 1, so a heading that would be shifted below level 1 becomes a regular paragraph. Exception: with a shift of -N, a level-N heading at the beginning of the document replaces the metadata title.--shift-heading-level-by=-1is a good choice when converting HTML or Markdown documents that use an initial level-1 heading for the document title and level-2+ headings for sections.--shift-heading-level-by=1may be a good choice for converting Markdown documents that use level-1 headings for sections to HTML, since pandoc uses a level-1 heading to render the document title.
So running pandoc with --shift-heading-level-by=-1 might be enough to suit your needs.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | Pavel |
| Solution 2 | sijinzac |
| Solution 3 | Kostas Minaidis |
| Solution 4 | Jacob |
| Solution 5 | |
| Solution 6 | tarleb |

