In this post: My investigation into why ESLint didn’t like my .forEach loop and why for...of
is the better choice for an asynchronous loop.
Key words: React, TypeScript, loops, JavaScript array iterators, forEach, for let, asynchronous
So there I was, .forEach
-ing through some post replies when I noticed the code was triggering an ES-Lint error, no-loop-func:
Line 73:28: Function declared in a loop contains unsafe references to variable(s) 'repliesTotal' no-loop-func
My code seemed to work fine as it was, though.
const processUnreadReplies = async (userPosts: Post[]) => {
let repliesTotal = 0;
for await (let post of userPosts) {
const replyArray: Array<Reply> = await getRepliesToPost(post.pid);
replyArray.forEach((reply: any) => {
if (!reply.read) {
post.unreadReplyCount = (post.unreadReplyCount ?? 0) + 1;
repliesTotal++;
}
});
};
setUnreadRepliesTotal(repliesTotal);
}
In English, this is what this code is doing:
- It gets all of the user’s posts as
userPosts
(and they are of typePost
) - It loops through each
post
inuserPosts
, getting that post’s replies asreplyArray
- For each
reply
inreplyArray
, it checks each one to see if it isread
or not by waiting on an asychronous function call - If the
reply
is notread
(reply.read is false), then it increases the post’sunreadReplyCount
- When it’s done with the loop, it sets a state variable called
unreadRepliesTotal
to the total it tallied up during the loop
ES-Lint’s documentation for no-loop-func was informative, but the examples didn’t make it clear enough to me what I had done wrong. Their “don’t do this” examples were "let i = 0, i < n; i++"
and do ... while
style loops, and I had a .forEach
. They also didn’t have anything on the topic of asynchronous loops.
(Could my code be so awful that they didn’t even consider including it as a “do not do this” example? :D )
I decided to investigate.
First, I had to rethink my assumption that .forEach was the preferred ES6 style – maybe it wasn’t always the case, and maybe my case was one of them.
Two helpful (ie: plain English) posts I read while researching this problem:
- Loops in JavaScript
- Should one use for-of or forEach when iterating through an array? <– this SO post is recent (2019) and arrives at the same solution I did
For...in
iterates through the enumerable properties of an object or array, which means it steps through all the X
in object[X]
or array[X]
. You can still use it to access the elements of an array like so:
for (const idx in cars) {
console.log(cars[idx]);
}
But that a more roundabout way of accessing the array data, and I soon found a more direct approach:
For...of
was the next thing I tried and it worked just as well as my .forEach
, but the linter liked it better.
const processUnreadReplies = async (userPosts: Post[]) => {
let repliesTotal = 0;
for await (let post of userPosts) {
const replyArray: Array<Reply> = await getRepliesToPost(post.pid);
for (let reply of replyArray) {
if (!reply.read) {
post.unreadReplyCount = (post.unreadReplyCount ?? 0) + 1;
repliesTotal++;
}
};
};
setUnreadRepliesTotal(repliesTotal);
}
But why? I went back to the MDN docs and discovered an important tidbit I overlooked earlier:
forEach expects a synchronous function
from the MDN docs on forEach
forEach
does not wait for promises. Kindly make sure you are aware of the implications while using promises (or async functions) asforEach
callback.
As far as ESLint could tell from looking at the code, every loop run by .forEach
was returning void
. Furthermore, using await
does not pause the .forEach
(I didn’t expect it to pause it, and I don’t need the previous iteration’s result for the next one, but I wanted to make note of that important distinction anyway).
In any case, this seemed like one of those times where the thing appeared to be working to the user, but could be done better and for...of
was the preferred approach here. (There are plenty of opinions and debates about this, though.)
In summary
- Use
for...of
for asynchronous loops in JavaScript/TypeScript let
creates a block-scoped variable, so in the case of my code the let is creating a new “reply” instance for each iteration of thefor...of
and each “reply” will be an individual reply from the array.forEach()
does not wait for asynchronous code to complete, which may not be a problem in your specific use case but ESLint can’t tell that so it flags it as a potential problemawait
suspends the current function until it is resolvedfor...of
creates an individual function call for each loop, whereas.forEach()
counts as one function call for all iterations
So there we have it – a better, ESLint-approved, way of writing an asynchronous loop in TypeScript/JavaScript.
Related
- See the replies on this StackOverflow post for more discussion of
.forEach
vs.for...of
- There is an ESLint package that adds a rule that alerts you to uses of async and forEach together
- ESLint’s docs for no-loop-func provide examples of what to do and what not to do