useEffect.dev
← Back to lessons

Lesson 7. Solving infinite loops when using useEffect 3. Array and object states

Common causes of infinite loops also include the use of arrays and objects as local state. It often happens when we want to update the state, or when we want to react to some change in the object’s attributes.

Effects using an array state

For the next case, we want to create a component receiving a prop (let’s call it value) and store the values history in an array.

export const PreviousValues = ({ value }) => {
const [previous, setPrevious] = useState([])
useEffect(() => {
if (value) {
setPrevious([...previous, value * 2])
}
}, [previous, value])
return <p>Previous values: [{previous.join(', ')}]</p>
}

In our useEffect, we define the new values of the previous array using its current value, to which we add our new value. Since we use previous and value in the effect function, we put these two values in the dependencies array.

Unfortunately, this code goes into an infinite loop. Each time we receive a value, we update the previous array, triggering a new execution of the effect function.

A way to solve this problem is to keep another local state, containing the previous value. If the current value is equal to the previous one, it means we already added it to the array:

const [lastValue, setLastValue] = useState(value)
useEffect(() => {
if (value && value !== lastValue) {
setPrevious([...previous, value])
setLastValue(value)
}
}, [value, lastValue, previous])

Another solution is to use the same trick we used to solve the issue of getting the current state in async effects in the previous section: using the alternative syntax to set a new local state.

This way, we don’t have to put previous in the dependencies array, so no more infinite loop:

useEffect(() => {
if (value) {
setPrevious((previous) => [...previous, value])
}
}, [value])

Effects using object state

For the last scenario that can cause an infinite loop, we want to create a Starship component with a local state starship: an object with an id attribute and an info attribute. We want each time the id attribute is updated (with an input field, for instance), we call a web service to get the info value: the information about the starship.

const [starship, setStarship] = useState({ id: 9 })
useEffect(() => {
fetchStarship(starship.id).then((starshipInfo) => {
setStarship({ id: starship.id, info: starshipInfo })
})
}, [starship])

Since we use the starship local state in the effect function, we add it to the dependencies array. But when we receive the result of the API call, we update starship, triggering a new API call, etc.

The most obvious way to fix the issue is to check if we already have the info in the starship object:

useEffect(() => {
if (!starship.info) {
fetchStarship(starship.id).then((starshipInfo) => {
setStarship({ id: starship.id, info: starshipInfo })
})
}
}, [starship])

What if we can’t? What if info has some data already, with no way to know if it is up to date? Let’s say we always have info for the spaceship but want to make the request regardless?

The key is that we want to make a new request each time the starship ID is updated, not the spaceship info. Until now, we have always put full local states or props in the dependencies array, but nothing forbids us to select only some attributes of these local states or props.

In our case, we only need to watch for changes in the id attribute of spaceship:

useEffect(() => {
fetchStarship(starship.id).then((starshipInfo) => {
setStarship({ id: starship.id, info: starshipInfo })
})
}, [starship.id])

Since the ID is not updated by the request, no infinite loop, problem solved!