Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

About infinite loop in useEffect

I am trying to build a redux process with react hooks, the code below is that I want to simulate a ComponentDidMount function with a getUsers(redux action) call in it which is a http request to fetch data.

The first version was like this

const { state, actions } = useContext(StoreContext);
const { getUsers } = actions;

useEffect(() => {
  getUsers();  // React Hook useEffect has a missing dependency: 'getUsers'.
}, []);

but I got a linting warning "React Hook useEffect has a missing dependency: 'getUsers'. Either include it or remove the dependency array" in useEffect,

and then I added getUsers to dependency array, but got infinite loop there

useEffect(() => {
  getUsers(); 
}, [getUsers])

Now I find a solution by using useRef

const fetchData = useRef(getUsers);

useEffect(() => {
  fetchData.current();
}, []);

Not sure if this is the right way to do this, but it did solve the linting and the infinite loop (temporarily?)

My question is: In the second version of the code, what exactly caused the infinite loop? does getUsers in dependency array changed after every render?

like image 398
boaol Avatar asked Dec 08 '19 12:12

boaol


People also ask

Why is useEffect infinite loop?

Passing no dependencies in a dependency array If your useEffect function does not contain any dependencies, an infinite loop will occur.

Can we use loop in useEffect?

The infinite loop is fixed by correct management of the useEffect(callback, dependencies) dependencies argument. Adding [value] as a dependency of useEffect(..., [value]) , the count state variable is updated only when [value] is changed. Doing so solves the infinite loop.

Why does useEffect run multiple times?

If your application is behaving strangely after updating to React 18, the default behavior of useEffect changed to run it 2 times. Just in development mode, but this is the mode everyone builds their application on.

Can we use useEffect two times?

If you have just made a new project using Create React App or updated to React version 18, you will notice that the useEffect hook is called twice in development mode. This is the case whether you used Create React App or upgraded to React version 18.


1 Answers

Your function have dependencies and React deems it unsafe not to list the dependencies. Say your function is depending on a property called users. Listing explicitly the implicit dependencies in the dependencies array won't work:

useEffect(() => {
  getUsers();
}, [users]); // won't work

However, React says that the recommended way to fix this is to move the function inside the useEffect() function. This way, the warning won't say that it's missing a getUsers dependency, rather the dependency/ies that getUsers depends on.

function Example({ users }) {

  useEffect(() => {
    // we moved getUsers inside useEffect
    function getUsers() {
      console.log(users);
    }
    getUsers();
  }, []); // users dependency missing
}

So you can then specify the users dependency:

useEffect(() => {
    function getUsers() {
      console.log(users);
    }
    getUsers();
  }, [users]); // OK

However, you're getting that function from the props, it's not defined in your component.

What to do then? The solution to your problem would be to memoize your function.

useCallback will return a memoized version of the callback that only changes if one of the dependencies has changed. This is useful when passing callbacks to optimized child components that rely on reference equality to prevent unnecessary renders (e.g. shouldComponentUpdate).

You can't memoize it within your component as there will be the same warning:

const memoizedGetUsers = useCallback(
  () => {
    getUsers();
  },
  [], // same warning, missing the getUsers dependency
);

The solution is to memoize it right where the getUsers is defined and you will be then be able to list the dependencies:

// wrap getUsers inside useCallback
const getUsers = useCallback(
  () => {
    //getUsers' implementation using users
    console.log(users);
  },
  [users], // OK
);

And in your component, you'll be able to do:

const { getUsers } = actions; // the memoized version

useEffect(() => {
    getUsers();
  }, [getUsers]); // it is now safe to set getUsers as a dependency


As to the reason why there was an infinite loop and why useRef worked. I'm guessing your function causes a rerender and at each iteration, getUsers was recreated which ends up in an endless loop. useRef returns an object { current: ... } and the difference between using useRef and creating this object { current: ... } yourself is that useRef returns the same object and doesn't create another one. So you were propbably using the same function.

like image 136
jperl Avatar answered Nov 15 '22 04:11

jperl