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.
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:
isLegacyCustomer
which tells us as a booleantrue
orfalse
whether or not the customer is considered legacy.name
which is the full name string of the user/customer.spend
which is the amount of money they've spent with us, made up of a total of theirinvoices
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.