tutorial // Jan 07, 2022

How to Use Map to Dynamically Modify an Array in JavaScript

How to use the .map() method in JavaScript to dynamically modify an array of objects.

How to Use Map to Dynamically Modify an Array in JavaScript

Getting Started

Because the code we're writing for this tutorial is "standalone" (meaning it's not part of a bigger app or project), we're going to create a Node.js project from scratch. If you don't already have Node.js installed on your computer, read this tutorial first and then come back here.

Once you have Node.js installed on your computer, from your projects folder on your computer (e.g., ~/projects), create a new folder for our work:

Terminal

mkdir map

Next, cd into that directory and create an index.js file (this is where we'll write our code for the tutorial):

Terminal

cd map && touch index.js

Next, in the same folder, run npm init -f to bootstrap a package.json file:

Terminal

npm init -f

This will tell NPM (Node Package Manager) to create a new package.json file in your app. The -f part stands for "force" and will skip the step-by-step wizard you see when running npm init by itself (feel free to use this to understand how it works).

Finally, we need to install two dependencies: dayjs and currency.js.

Terminal

npm i dayjs currency.js

We'll use these two to do some analysis and formatting on our data as part of our .map().

With that, we're ready to get started.

Adding in user data

Our goal for this tutorial is to use the Array.map() method in JavaScript to format some user data and help us understand who our most valuable users are. To start, let's add some test data in a separate file at the root of our project:

/users.js

export default [
  {
    "createdAt": "2021-12-08T16:20:14+00:00",
    "invoices": [
      {
        "createdAt": "2021-12-08T16:20:14+00:00",
        "amount": 790.31
      },
      {
        "createdAt": "2021-12-07T16:20:14+00:00",
        "amount": 893.38
      },
      {
        "createdAt": "2021-12-06T16:20:14+00:00",
        "amount": 302.97
      },
      ...
    ],
    "name": {
      "first": "Wester",
      "last": "Christian"
    },
    "emailAddress": "wester.christian@gmail.com"
  },
  ...
];

Note: this is a shortened list as the real list (available here on Github) is fairly long.

Once you have that in the app, we're ready to move on to writting our .map() function over this array.

Mapping over the users array

To start, let's build out a skeleton for our .map() function and review and discuss how it's going to work:

/index.js

import users from './users.js';

const spendByUser = users.map((user) => {
  // We'll return our modified user here...
});

console.log(spendByUser);

Back in our /index.js file, here, we import our /users.js file as users (remember, we have an export default in that file so we can just say import users in our code—if this were a named export, we'd see something like import { users } from '...').

Because we know that users variable should contain an array (what we exported from /users.js), we can call .map() directly on it. This is because .map() is a built-in function in JavaScript. It's defined on the Array prototype (the name used for the object that contains the functionality inherited by a function in JavaScript). We say capital "A" Array here because that is the function in JavaScript that defines the behavior of an array. As part of its prototype object, we have the .map() function (known as a method because it's a function defined on an existing object).

Like its siblings, .map() allows us to perform a loop over an array and do something. The something in this case is to modify elements in an array and return them, creating a new array with the modified elements. A quick example:

const greetings = ['Hello', 'Goodbye', 'See ya'];

const greetingsWithName = greetings.map((greeting) => {
  return `${greeting}, Ryan!`
});

console.log(greetingsWithName);

// ['Hello, Ryan!', 'Goodbye, Ryan!', 'See ya, Ryan!']

Here we take an array of strings and use .map() to loop over that array. For each string in the array, we return a new string (created using backticks so we can leverage JavaScript's string interpolation). In return to our call to greetings.map() we get a new array. It's important to understand: this is a brand new, unique array. The .map() function creates a copy of whatever array we call the .map() function on and returns that new array.

Here, we store that in a variable greetingsWithName and then console.log() it out to see the modified copy.

/index.js

import dayjs from 'dayjs';
import users from './users.js';

const spendByUser = users.map((user) => {
  return {
    isLegacyCustomer: dayjs(user?.createdAt).isAfter(dayjs().subtract(60, 'days')),
    name: `${user?.name?.first} ${user?.name?.last}`,
  };
});

console.log(spendByUser);

Now that we understand the fundamentals of .map(), let's start to modify our users array. The same exact principles are in play as we saw above: we take an array, call .map() on it, and get a new array in return.

Up top, notice that above our users import we've imported one of the dependencies we installed earlier: dayjs. Down in our .map() function, in the callback we pass to .map(), we're returning an object. Our goal here is to do some "analysis" on each user and figure out how much each customer has spent and whether or not they're a legacy customer.

Notice: we don't have to return the exact same object shape (or even an object for that matter) from our .map(). We just need to return whatever we want to take place of the current item we're mapping over in the array.

Here, we want to create a new object that has three properties:

  1. isLegacyCustomer which tells us as a boolean true or false whether or not the customer is considered legacy.
  2. name which is the full name string of the user/customer.
  3. spend which is the amount of money they've spent with us, made up of a total of their invoices array.

Here, we're focusing just on isLegacyCustomer and name (spend is a bit more complicated so we'll add that next).

For isLegacyCustomer, we want to know if the user was created in our database more than 60 days ago (we're just pretending here). To find out, we take the createdAt property on the user object and pass it to dayjs() (the function we imported from the package of the same name up top).

dayjs is a library for manipulating and working with dates. Here, to make our work easier, we use dayjs() to tell us if the timestamp we passed it (user.createdAt) is after another date we're creating on the fly with another call to dayjs: dayjs().subtract(60, 'days'). Here, we get back a dayjs object containing a date 60 days before now.

In response, we expect the .isAfter() function in dayjs to return us a boolean true or false.

For the name field, we create a string using the same backtick pattern we saw earlier, this time using interpolation to concatenate (join together) the first and last name of our user (using the name.first and name.last properties from the name object on the user).

/index.js

import dayjs from 'dayjs';
import currency from 'currency.js';
import users from './users.js';

const spendByUser = users.map((user) => {
  return {
    isLegacyCustomer: dayjs(user?.createdAt).isAfter(dayjs().subtract(60, 'days')),
    name: `${user?.name?.first} ${user?.name?.last}`,
    spend: user?.invoices?.reduce((total, invoice) => {
      total += invoice.amount;
      return currency(total, { precision: 2 }).value;
    }, 0),
  };
});

console.log(spendByUser);

Now for the tricky part. In order to get the spend property for our users, we need to use another array method .reduce() to loop over the user.invoices array. Similar to a .map(), the .reduce() method loops over or iterates through the array the method is called on.

If you want to check out a more in-depth review of how to use .reduce() in JavaScript give this tutorial a read.

Instead of returning a new array, however, a .reduce() method returns whatever value we assign to the acc or "accumulator." The accumulator in a reduce function is a value that starts as some value and—if we're using .reduce() for its intended purpose—returns a modified or "updated" version of that value.

Here, the accumulator starts as the 0 passed as the second argument to user?.invoices?.reduce() (the question marks there are just saying "if user exists, and invoices exists on that, call .reduce() on user.invoices"). For each loop or iteration of user.invoices, the current value of the accumulator (again, starting as that 0) is passed as the first argument to the function we pass to .reduce(), here labeled as total. As the second argument, we get access to the current item in the array being looped over.

If we look at our code here, our goal is to "total up" the invoice.amount field for each object in the user.invoices array.

For each iteration of our .reduce(), we take the current value of total and add the current invoice.amount to it. Next, we take the resulting total and pass it to the currency() function that we imported from currency.js up at the top of our file. This helps us to format the currency value properly as a float number (e.g., 123.45). To that function, we pass total as the first argument and then an options object for the function with precision: 2 as a property, saying "format this number to two decimal places."

Finally, we return the .value property on the object returned by the call to currency(total, { precision: 2 }). What we return here becomes the new or "updated" value for the accumulator which will be available as total on the next loop/iteration of user?.invoices. So it's clear, total in our code here will get the following for each iteration with this example array (assuming we start at 0):

[{ amount: 1 }, { amount: 2.55 }, { amount: 3.50 }]

total = 0 // first item
total = 1
total = 3.55
total = 7.05 // last item

Once our .reduce() completes, we expect to get back the final value of total (after the last item has been added) in return. This means that spend should contain the total spend for each of our users.

That's it! If we give this a spin (making sure to log out spendByUser at the bottom of our file), we should get something like this:

[
  { isLegacyCustomer: true, name: 'Wester Christian', spend: 10729.91 },
  { isLegacyCustomer: true, name: 'Carthon Weaver', spend: 14926.53 },
  { isLegacyCustomer: true, name: 'Keldrin Durham', spend: 13491.61 },
  { isLegacyCustomer: true, name: 'Jurgen Espinosa', spend: 13179.59 },
  ...
]

To finish up, let's take a look at how to make use of this data.

Sorting based on mapped data

So why would we want to do something like this? Like most things, it depends on our code and the project we're working on. To add context, though, we could assume that we're trying to find a customer to reward based on their total spend with our company. Now that we have our mapped array handy, we can do something like this:

/index.js

import dayjs from 'dayjs';
import currency from 'currency.js';
import users from './users.js';

const spendByUser = users.map((user) => { ... });

const mostValuableCustomer = spendByUser.sort((a, b) => a.spend - b.spend).pop();
console.log({ mostValuableCustomer });

Here, we've added two lines. We've created a variable mostValueCustomer and to it, we're setting the result of calling the .sort() method (another Array prototype method) and passing it a function. That function takes the current item and the next item in the array and compares them to find which should come first. Here, we sort by the total spend to say "start with the least spend on top and finish with the most spend on the bottom."

With that result (we expect to get back the sorted copy of our spendByUser array), we call the .pop() method to say "pop off the last item in the array and return it." Here, that last item (the customer with the highest spend) is stored in the mostValuableCustomer variable. If we log this variable out, here's what we should get when we run our code:

{ mostValuableCustomer: { isLegacyCustomer: false, name: 'Vicente Henry', spend: 15755.03 } }

Wrapping up

In this tutorial, we learned how to use the Array.map method to loop over an existing array and modify its contents. We also learned how to format data as part of that .map() as well as how to use the resulting data in our code.

Written By
Ryan Glover

Ryan Glover

CEO/CTO @ CheatCode