zwl1619
zwl1619

Reputation: 4232

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?

https://i.sstatic.net/SHLjh.gif

Upvotes: 4

Views: 2928

Answers (5)

H. Almidan
H. Almidan

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:

  1. Arrays are JavaScript objects.
  2. JavaScript compares objects based on reference, not value. Meaning:
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.
  1. 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 (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:

  • Using 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.

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.

Upvotes: 0

Greg Domorski
Greg Domorski

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

Jacob
Jacob

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

sijinzac
sijinzac

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

Pavel
Pavel

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

Related Questions