Check out "Do you speak JavaScript?" - my latest video course on advanced JavaScript.
Language APIs, Popular Concepts, Design Patterns, Advanced Techniques In the Browser

forEach or not to forEach

I had an interesting bug in my React application. It happened that the problem was in the fact that I was using forEach instead of for.

The bug that I was encounter was

Warning: Can't perform a React state update on an unmounted component.
This is a no-op, but it indicates a memory leak in your application.
To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.

It took me some time to understand why is this happening. The reason was because I had an array of functions which I modified while I was looping over it. My array was filled with hooks state setters. Imagine that the first item of the array updates the root React component which results in unmounting of some deeply nested component. Once that component is unmounted I update that same array so I remove the state setter. That is because the component doesn't exists in the tree and doesn't make sense to update it. A clean up sort of thing. Or in other words I was removing an item at let's say position 5 while the iteration is at position 1. Interestingly enough the setter at position 5 was still executed and React warns me that there is nothing to update and this may be a memory leak.

The actual problem was in the fact that I used forEach instead of for. forEach kind of works with the array that exists at the time of the first iteration. Even if we change the array the operator still loops over all the elements. Consider the following example where we want to remove the fifth element when we are at the third one:

var a = [1, 2, 3, 4, 5];

a.forEach(function (item) {
  console.log(item);
  if (item === 3) {
    a = a.filter(n => n !== 5);
  }
});

The result of it is a print of all the five numbers even though at index three we remove the last one.

1
2
3
4
5

Now let's see the same code but done with the for operator:

var a = [1, 2, 3, 4, 5];

for(let i = 0; i < a.length; i++) {
  console.log(a[i]);
  if (a[i] === 3) {
    a = a.filter(n => n !== 5);
  }
}

The result is more like what I expected to see.

1
2
3
4

Because at every iteration we look at the momental value of the variable a we operate with its up-to-date version.

P.S. I know that such mutation of an array while looping over it is not a good idea. However, in my case is safe because I for sure know that I'll modify only items after the index of mutation.

If you enjoy this post, share it on Twitter, Facebook or LinkedIn.