20
Are you using Array.map correctly?
Lately, I've noticed a trend towards the inappropriate use of Array.map
, both in tutorials and production code. I'm not sure exactly why this is happening, but I think it may stem from the prevalence of Array.map
in React JSX components. JSX code typically uses Array.map
to render a component hierarchy for each item in an array.
import React from 'react';
import PropTypes from 'prop-types';
const ItemsList = (props) => {
const { items = [] } = props;
return (
<ul className="items-list">
{items.map((item) => (
<li key={item.id}>{item.content}</li>
))}
</ul>
);
};
ItemsList.propTypes = {
items: Proptypes.arrayOf(
PropTypes.shape({
id: PropTypes.string.isRequired,
content: PropTypes.node.isRequired,
})
).isRequired,
};
export default ItemsList;
The example above shows a component that renders an unordered list from an array of objects (named items
). Each item in the list is rendered with the content
property of the current object (item
) as the items
array is enumerated. If you've worked with React or similar rendering libraries before, this should be familiar. But a few details might not be completely obvious:
- The
items.map
statement returns an array. - The returned array is used, even though it isn't assigned to a variable or constant.
This can made more explicit by rewriting the component as follows:
const ItemsList = (props) => {
const { items = [] } = props;
const listItems = items.map((item) => (
<li key={item.id}>{item.content}</li>
));
return (
<ul className="items-list">
{listItems}
</ul>
);
};
There is no behavioural difference between this and the previous version of ItemsList
. The only change is that the items.map
statement's return value is now assigned to const listItems
before rendering. Whether you use one approach over the other in practice is mostly a question of style. The point of this example is to make the render flow more explicit. In either case, the items
array is enumerated by the items.map
statement, which returns an array of JSX components to be rendered.
The React/JSX example demonstrates the correct use of Array.map
— as a means of transforming the contents of an array. Here are some conventional examples:
// Multiply each number in an array by 2:
const numbers = [1,2,3,4,5].map(n => n * 2);
console.log(numbers);
// Result:
// [2,4,6,8,10]
// Get full names for an array of people:
const guitarists = [
{ firstName: 'Bill', lastName: 'Frisell' },
{ firstName: 'Vernon', lastName: 'Reid' },
];
const names = guitarists.map((guitarist) => {
const { firstName, lastName } = guitarist;
return `${firstName} ${lastName}`;
});
console.log(names);
// Result:
// ['Bill Frisell', 'Vernon Reid']
// Add the full names to an array of people:
const guitaristsWithFullNames = guitarists.map((guitarist) => {
const { firstName, lastName } = guitarist;
return { ...guitarist, fullName: `${firstName} ${lastName}` };
});
console.log(guitaristsWithFullNames);
// Result:
/*
[
{ firstName: 'Bill', lastName: 'Frisell', fullName: 'Bill Frisell' },
{ firstName: 'Vernon', lastName: 'Reid', fullName: 'Vernon Reid' },
]
*/
Now that we've looked at some appropriate use cases for Array.map
, let's look at an inappropriate use case:
[1,2,3,4,5].map((number) => console.log(number));
This is a trivial example that uses Array.map
to execute a side effect for each item in an array. (In this case, the side effect is a call to console.log
but that doesn't matter. You could substitute any other function.) This code works and is familiar to beginning JavaScript developers because it's used so frequently in JSX, so what's wrong with it?
To put it simply, just because something works doesn't always mean it's correct or appropriate. The use of Array.map
in Example 4 is inappropriate because it doesn't conform to the method's intended purpose. True to its name, Array.map
is intended to be used to map (or transform) data from one structure to another. All the appropriate use cases we looked at follow this pattern:
- An array of data is mapped to an array of JSX components.
- An array of numbers is mapped to an array of the same numbers multiplied by 2.
- An array of objects representing people is converted to (or extended with) their full names.
Using Array.map
for anything other than mapping creates a few problems. First and foremost, it makes the code less clear. A developer reading code should expect Array.map
to perform some kind of transform and for the return value to be used. Second, the return value is always created whether you use it or not. In Example 4, the Array.map
callback returns undefined
. That means the Array.map
statement returns an array containing an undefined
value for each index — [1,2,3,4,5]
maps to [undefined, undefined, undefined, undefined, undefined]
. Aside from being messy, there are performance costs associated with the unnecessary creation and disposal of those unused return values. Used in this way, Array.map
is slower than the appropriate alternative, Array.forEach
.
If we rewrite Example 4 using Array.forEach
instead of Array.map
, we eliminate these problems:
[1,2,3,4,5].forEach((number) => console.log(number));
Array.forEach
is intended to be used this way and does not create an unnecessary return value for each item in the array. It's both clearer and faster.
Array.forEach
is similar in concept to a traditional for
loop, but has a few practical advantages. The obvious advantages are clarity and brevity. I think we can all agree that Array.forEach
is easier to read and write:
// Array.forEach:
[1,2,3,4,5].forEach((number) => console.log(number));
// for loop:
const numbers = [1,2,3,4,5];
for (let i = 0; i < numbers.length; i++) {
console.log(numbers[i]);
}
Another hidden advantage is that Array.forEach
handles arrays with uninitialized or deleted indexes (sparse arrays) gracefully. Consider the following example, which enumerates an array whose third index is uninitialized:
const numbers = [1,2,,4,5];
// Array.forEach:
numbers.forEach((number, i) => console.log(`number: ${number}, index: ${i}`));
// Result:
// number 1, index 0
// number 2, index 1
// number 4, index 3
// number 5, index 4
// for loop:
for (let i = 0; i < numbers.length; i++) {
console.log(`number: ${numbers[i]}, index: ${i}`);
}
// Result:
// number 1, index 0
// number 2, index 1
// number undefined, index 2
// number 4, index 3
// number 5, index 4
Notice that Array.forEach
skips the uninitialized index, thereby avoiding any problems that may be created by working on an uninitialized (or deleted) value. In most cases this is a nice safety feature to have. The for
loop is unforgiving and enumerates uninitialized values just like initialized values. If you want to ignore them, you have to do it manually. The side effect of having Array.forEach
perform those checks for you automatically is that it's somewhat slower than a for
loop written without checks. Before you go rewriting all your Array.forEach
code with for
loops to try to save a thousandth of a millisecond, keep in mind that the performance hit is negligible in the vast majority of real world use cases.
Why should you choose Array.map
, Array.forEach
or a traditional for
loop?
- Choose
Array.map
if you need to create a new array containing transformations of the items in the source array. - Choose
Array.forEach
when you simply need to enumerate the values in an array. - Choose a
for
loop only if absolute performance is critical (at the expense of stability and readability) or you still have to support Internet Explorer 8 (in which case you have my sympathy).
20