Are you using Array.map correctly?

August 31, 2021

Tony Wallace

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:


-- CODE language-javascript --
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} />;
	  	  ))}
	  	</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:


-- CODE language-js --
const ItemsList = (props) => {
	  const { items = [] } = props;
	  const listItems = items.map((item) => (
  	  	<li key={item.id}>{item.content}/>;
  	  ));

	  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:


-- CODE language-js --

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


-- CODE language-js --

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


-- CODE language-js --

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


-- CODE language-js --

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


-- CODE language-js --

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

Latest Posts

Be the first to know whats new at RedBit and in the industry. Plus zero spam.

Thank you! An email has been sent to confirm your subscription.
Oops! Something went wrong while submitting the form.

Let's Make Something Great Together

Contact Us