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!