Are you using Array.map correctly?

Array.map - Appropriate and Inappropriate Uses

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.

Example 1:

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:

  1. The items.map statement returns an array.
  2. 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:

Example 2:

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:

Example 3:

// 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:

Example 4:

[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.

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 vs. the for loop

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:

Example 5:

// 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:

Example 6:

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.

Summary:

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