Infinite loop in useEffect
Asked Answered
P

17

202

I've been playing around with the new hook system in React 16.7-alpha and get stuck in an infinite loop in useEffect when the state I'm handling is an object or array.

First, I use useState and initiate it with an empty object like this:

const [obj, setObj] = useState({});

Then, in useEffect, I use setObj to set it to an empty object again. As a second argument I'm passing [obj], hoping that it wont update if the content of the object hasn't changed. But it keeps updating. I guess because no matter the content, these are always different objects making React thinking it keep changing?

useEffect(() => {
  setIngredients({});
}, [ingredients]);

The same is true with arrays, but as a primitive it wont get stuck in a loop, as expected.

Using these new hooks, how should I handle objects and array when checking weather the content has changed or not?

Preteritive answered 30/10, 2018 at 18:45 Comment(3)
Tobias, what use case requires changing the value of ingrediants, once it's value has changed?Inhambane
@Tobias, you should read my answer. I am sure you will accept it as the correct answer.Chalybeate
I read this article and it helped me understand something more clearly. What I'd do is look up specific attributes about the object/ array, like the number of elements, or a name (whatever you like) and use these as dependencies in the useEffect hookSuddenly
P
222

Passing an empty array as the second argument to useEffect makes it only run on mount and unmount, thus stopping any infinite loops.

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

This was clarified to me in the blog post on React hooks at https://www.robinwieruch.de/react-hooks/

Preteritive answered 30/10, 2018 at 23:53 Comment(8)
Its actually an empty array not an empty object you should pass in.Flagstad
This doesn't solve the issue, you need to pass dependencies used by the hookPhototonus
Note that on unmount the effect will run a cleanup function if you have specified one. The actual effect doesn't run on unmount. reactjs.org/docs/hooks-effect.html#example-using-hooks-1Scute
Using empty array when setIngrediets is a dependency is an antipattern as Dan Abramov said. Don't treat useEffetct like a componentDidMount() methodOriginate
DON'T USE IT. You will get exhaustive deps error: github.com/facebook/react/issues/14920Hardly
since the people above didn't provide a link or resource on how to correctly solve this problem, I suggest oncoming duckduckgoers to check this out, since it fixed my infinite loop problem: #56658407Trotskyism
Can you help me with a similar problem? #67774360Zenobiazeolite
This is a poor answer because now useEffect will on ever run once. There needs to be some explanation of dependencies. My understanding is that you have to pass some sort of state for proper behavior unless you are absolutely sure you never want this working again until the whole component re-renders.Bobby
T
132

Had the same problem. I don't know why they not mention this in docs. Just want to add a little to Tobias Haugen answer.

To run in every component/parent rerender you need to use:

  useEffect(() => {

    // don't know where it can be used :/
  })

To run anything only one time after component mount(will be rendered once) you need to use:

  useEffect(() => {

    // do anything only one time if you pass empty array []
    // keep in mind, that component will be rendered one time (with default values) before we get here
  }, [] )

To run anything one time on component mount and on data/data2 change:

  const [data, setData] = useState(false)
  const [data2, setData2] = useState('default value for first render')
  useEffect(() => {

// if you pass some variable, than component will rerender after component mount one time and second time if this(in my case data or data2) is changed
// if your data is object and you want to trigger this when property of object changed, clone object like this let clone = JSON.parse(JSON.stringify(data)), change it clone.prop = 2 and setData(clone).
// if you do like this 'data.prop=2' without cloning useEffect will not be triggered, because link to data object in momory doesn't changed, even if object changed (as i understand this)
  }, [data, data2] )

How i use it most of the time:

export default function Book({id}) { 
  const [book, bookSet] = useState(false) 

  const loadBookFromServer = useCallback(async () => {
    let response = await fetch('api/book/' + id)
    response  = await response.json() 
    bookSet(response)
  }, [id]) // every time id changed, new book will be loaded

  useEffect(() => {
    loadBookFromServer()
  }, [loadBookFromServer]) // useEffect will run once and when id changes


  if (!book) return false //first render, when useEffect did't triggered yet we will return false

  return <div>{JSON.stringify(book)}</div>  
}
Tabb answered 31/10, 2018 at 20:8 Comment(6)
According to the React faq, it is not safe to omit functions from the list of dependencies.Tartuffery
@endavid depends on what is the prop you are usingTabb
of course, if you don’t use any values from the component scope, then it is safe to omit. But from a purely designing/architecturing point of vue, this is not a good practice, since it requires you to move your entire function inside of the effect if you need to use props and you could endup with an useEffect method that will be using an outrageous amount of lines of code.Tartuffery
This is really helpful, calling the asynchronous function inside useEffect hook only updates when the value change. Thanks mateDictatorship
@egdavid so then what is the proper way of running a certain block of code when a value updates? Using UseEffect causes a lint error to include the value in the dependancy but that causes in infinite loop, so that method can't be usedMotteo
Wow I assumed passing no argument would be the same as passing an empty array... What a headache.Homogony
E
37

I ran into the same problem too once and I fixed it by making sure I pass primitive values in the second argument [].

If you pass an object, React will store only the reference to the object and run the effect when the reference changes, which is usually every singe time (I don't now how though).

The solution is to pass the values in the object. You can try,

const obj = { keyA: 'a', keyB: 'b' }

useEffect(() => {
  // do something
}, [obj.keyA, obj.keyB]);
Ericaericaceous answered 9/2, 2019 at 4:32 Comment(4)
Another approach is to create such values with useMemo, that way the reference is kept and dependencies' values are evaluated as the samePhototonus
@Phototonus you can use useMemo() for values or useCallback() for functionsPearsall
Is that random values being passed to the dependency array? Of course this prevents the infinite loop but is this encouraged? I could pass 0 to the dependency array as well?Advertence
@Dinesh the first solution would work if you added a spread operator }, ...[Object.values(obj)]); ... or simply remove the square brackets }, Object.values(obj)); ...Worry
S
28

If you are building a custom hook, you can sometimes cause an infinite loop with default as follows

function useMyBadHook(values = {}) {
    useEffect(()=> { 
           /* This runs every render, if values is undefined */
        },
        [values] 
    )
}

The fix is to use the same object instead of creating a new one on every function call:

const defaultValues = {};
function useMyBadHook(values = defaultValues) {
    useEffect(()=> { 
           /* This runs on first call and when values change */
        },
        [values] 
    )
}

If you are encountering this in your component code the loop may get fixed if you use defaultProps instead of ES6 default values

function MyComponent({values}) {
  useEffect(()=> { 
       /* do stuff*/
    },[values] 
  )
  return null; /* stuff */
}

MyComponent.defaultProps = {
  values = {}
}
Strickland answered 29/10, 2019 at 6:41 Comment(1)
Thanks! This was non-obvious to me and exactly the problem I was running into.Obreption
I
14

Your infinite loop is due to circularity

useEffect(() => {
  setIngredients({});
}, [ingredients]);

setIngredients({}); will change the value of ingredients(will return a new reference each time), which will run setIngredients({}). To solve this you can use either approach:

  1. Pass a different second argument to useEffect
const timeToChangeIngrediants = .....
useEffect(() => {
  setIngredients({});
}, [timeToChangeIngrediants ]);

setIngrediants will run when timeToChangeIngrediants has changed.

  1. I'm not sure what use case justifies change ingrediants once it has been changed. But if it is the case, you pass Object.values(ingrediants) as a second argument to useEffect.
useEffect(() => {
  setIngredients({});
}, Object.values(ingrediants));
Inhambane answered 29/6, 2019 at 14:8 Comment(0)
K
13

As said in the documentation (https://reactjs.org/docs/hooks-effect.html), the useEffect hook is meant to be used when you want some code to be executed after every render. From the docs:

Does useEffect run after every render? Yes!

If you want to customize this, you can follow the instructions that appear later in the same page (https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects). Basically, the useEffect method accepts a second argument, that React will examine to determine if the effect has to be triggered again or not.

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // Only re-run the effect if count changes

You can pass any object as the second argument. If this object remains unchanged, your effect will only be triggered after the first mount. If the object changes, the effect will be triggered again.

Kimberly answered 11/2, 2019 at 18:2 Comment(0)
R
13

I'm not sure if this will work for you but you could try adding .length like this:

useEffect(() => {
        // fetch from server and set as obj
}, [obj.length]);

In my case (I was fetching an array!) it fetched data on mount, then again only on change and it didn't go into a loop.

Rento answered 27/4, 2019 at 16:9 Comment(1)
What if an item is replaced in the array? In that case the length of the array would be the same and the effect wouldn't run!Device
M
11

If you include empty array at the end of useEffect:

useEffect(()=>{
        setText(text);
},[])

It would run once.

If you include also parameter on array:

useEffect(()=>{
            setText(text);
},[text])

It would run whenever text parameter change.

Motorist answered 2/3, 2020 at 9:8 Comment(4)
why it would run only once if we put an empty array at the end of a hook?Bohemianism
An empty array at the end of a useEffect is a purposeful implementation by the developers to stop infinite loops in situations where you may, for instance, need to setState inside of a useEffect. This would otherwise lead to useEffect -> state update -> useEffect -> infinite loop.Whitt
An empty array causes a warning from eslinter.Nervous
Here is a detailed explanation about that: reactjs.org/docs/…Finsen
M
8

I often run into an infinite re-render when having a complex object as state and updating it from useRef:

const [ingredients, setIngredients] = useState({});

useEffect(() => {
  setIngredients({
    ...ingredients,
    newIngedient: { ... }
  });
}, [ingredients]);

In this case eslint(react-hooks/exhaustive-deps) forces me (correctly) to add ingredients to the dependency array. However, this results in an infinite re-render. Unlike what some say in this thread, this is correct, and you can't get away with putting ingredients.someKey or ingredients.length into the dependency array.

The solution is that setters provide the old value that you can refer to. You should use this, rather than referring to ingredients directly:

const [ingredients, setIngredients] = useState({});

useEffect(() => {
  setIngredients(oldIngedients => {
    return {
      ...oldIngedients,
      newIngedient: { ... }
    }
  });
}, []);
Mantelet answered 7/2, 2022 at 8:17 Comment(1)
This is the real answer, for those of us in a situation where we really do need to modify state and so can't list it as a dependency or we get an infinite loop.Catechin
S
3

If you use this optimization, make sure the array includes all values from the component scope (such as props and state) that change over time and that are used by the effect.

I believe they are trying to express the possibility that one could be using stale data, and to be aware of this. It doesn't matter the type of values we send in the array for the second argument as long as we know that if any of those values change it will execute the effect. If we are using ingredients as part of the computation within the effect, we should include it in the array.

const [ingredients, setIngredients] = useState({});

// This will be an infinite loop, because by shallow comparison ingredients !== {} 
useEffect(() => {
  setIngredients({});
}, [ingredients]);

// If we need to update ingredients then we need to manually confirm 
// that it is actually different by deep comparison.

useEffect(() => {
  if (is(<similar_object>, ingredients) {
    return;
  }
  setIngredients(<similar_object>);
}, [ingredients]);

Standardize answered 22/8, 2019 at 15:53 Comment(0)
G
3

The main problem is that useEffect compares the incoming value with the current value shallowly. This means that these two values compared using '===' comparison which only checks for object references and although array and object values are the same it treats them to be two different objects. I recommend you to check out my article about useEffect as a lifecycle methods.

Gummite answered 23/3, 2021 at 10:19 Comment(0)
G
2

The best way is to compare previous value with current value by using usePrevious() and _.isEqual() from Lodash. Import isEqual and useRef. Compare your previous value with current value inside the useEffect(). If they are same do nothing else update. usePrevious(value) is a custom hook which create a ref with useRef().

Below is snippet of my code. I was facing problem of infinite loop with updating data using firebase hook

import React, { useState, useEffect, useRef } from 'react'
import 'firebase/database'
import { Redirect } from 'react-router-dom'
import { isEqual } from 'lodash'
import {
  useUserStatistics
} from '../../hooks/firebase-hooks'

export function TMDPage({ match, history, location }) {
  const usePrevious = value => {
    const ref = useRef()
    useEffect(() => {
      ref.current = value
    })
    return ref.current
  }
  const userId = match.params ? match.params.id : ''
  const teamId = location.state ? location.state.teamId : ''
  const [userStatistics] = useUserStatistics(userId, teamId)
  const previousUserStatistics = usePrevious(userStatistics)

  useEffect(() => {
      if (
        !isEqual(userStatistics, previousUserStatistics)
      ) {
        
        doSomething()
      }
     
  })
Goddamned answered 6/9, 2019 at 13:58 Comment(3)
I guess people downvoted this because you suggested to use a third party library. However, the underlying idea is good.Sensibility
Doesn't this still create an infinite loop (albeit with an almost empty body), so that the browser will eat a lot of CPU cycles?Richey
@Richey Do you have any better solution?Goddamned
C
2

In case you DO need to compare the object and when it is updated here is a deepCompare hook for comparison. The accepted answer surely does not address that. Having an [] array is suitable if you need the effect to run only once when mounted.

Also, other voted answers only address a check for primitive types by doing obj.value or something similar to first get to the level where it is not nested. This may not be the best case for deeply nested objects.

So here is one that will work in all cases.

import { DependencyList } from "react";

const useDeepCompare = (
    value: DependencyList | undefined
): DependencyList | undefined => {
    const ref = useRef<DependencyList | undefined>();
    if (!isEqual(ref.current, value)) {
        ref.current = value;
    }
    return ref.current;
};

You can use the same in useEffect hook

React.useEffect(() => {
        setState(state);
    }, useDeepCompare([state]));
Chalybeate answered 19/4, 2020 at 13:21 Comment(2)
Where does the isEqual function come from?Loadstar
@JoshBowden lodashOrangutan
W
2

You could also destructure the object in the dependency array, meaning the state would only update when certain parts of the object updated.

For the sake of this example, let's say the ingredients contained carrots, we could pass that to the dependency, and only if carrots changed, would the state update.

You could then take this further and only update the number of carrots at certain points, thus controlling when the state would update and avoiding an infinite loop.

useEffect(() => {
  setIngredients({});
}, [ingredients.carrots]);

An example of when something like this could be used is when a user logs into a website. When they log in, we could destructure the user object to extract their cookie and permission role, and update the state of the app accordingly.

Whitt answered 24/7, 2020 at 10:17 Comment(0)
C
1

my Case was special on encountering an infinite loop, the senario was like this:

I had an Object, lets say objX that comes from props and i was destructuring it in props like:

const { something: { somePropery } } = ObjX

and i used the somePropery as a dependency to my useEffect like:


useEffect(() => {
  // ...
}, [somePropery])

and it caused me an infinite loop, i tried to handle this by passing the whole something as a dependency and it worked properly.

Calamondin answered 27/9, 2020 at 10:1 Comment(0)
D
0

Another worked solution that I used for arrays state is:

useEffect(() => {
  setIngredients(ingredients.length ? ingredients : null);
}, [ingredients]);
Deforest answered 11/5, 2021 at 23:13 Comment(0)
O
0

Here's a nasty similar case I had, with a snackbar (toast style UI hook).

Sometimes the variable isn't an obvious object that you're looking for.

const snackbar = useSnackbar();
useEffect(() => {
  try {
    // ... lots of code
  } catch (e) {
    snackbar.addAlert(e);
  }
}, [/* other dependencies, */snackbar]);

If lots of code triggers an error then you get an infinite loop, because snackbar.addAlert triggers a re-render. But you don't find this bug until you get errors.

useSnackbar is a hook so it can't be wrapped in useCallback. The only solution I found for this was to remove snackbar from the dependencies and add ignore the react-hooks/exhaustive-deps warning.

const snackbar = useSnackbar();
useEffect(() => {
  try {
    // ... lots of code
  } catch (e) {
    snackbar.addAlert(e);
  }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [/* other dependencies, */]);
Olivette answered 15/2 at 17:52 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.