Array 'Superpowers' π¦ΈπΎ
Manav Misra
Posted on November 1, 2020
Overview
Each of the methods covered in this post is incredibly powerful, in that they allow us to write less code that does more.
This is especially true as we start to work with 'realistic' sets of data - usually arrays of object literals.
The 'Underling' Superpowers
These first 3 are somewhat useful for specific tasks. If we are going with this π¦Έπ½ββοΈ analogy, these might be the 'apprentices,' - 'padawans,' if you will.
find
Use find
to..find and return the first element in an array that meets some true/false condition.
const todos = [
{
"id": 1,
"text": "Take out the trash",
"completed": false
},
{
"id": 2,
"text": "Shop at Fresh Thyme",
"completed": false
},
{
"id": 3,
"text": "Call Mom",
"completed": true
},
{
"id": 4,
"text": "Mow lawn",
"completed": true
},
{
"id": 5,
"text": "Litter Boxes Cleanup",
"completed": false,
"assignee": "Mary"
}
]
// Find the first 'completed' task
todos.find(function(todo) {
return todo.completed
})
We can rewrite using a 'one line' fat arrow: todos.find(todo => todo.completed)
The π to understanding this is the predicate callback function: (todo => todo.completed).
A predicate callback function is nothing but a callback that return
s based on upon coercion to either true
or false
.
In other words:
-
Iterate over
todos
, passing in each 'todo' one at a time. - Have a look π at
todo.completed
. If it'strue
,return
thistodo
. If not, just move on.
find
will stop iterating as soon as it finds the first 'match' It returns one single element from that array (if there is one that meets the _predicate condition)._
Refactor β»οΈ with Destructuring
todos.find(({completed}) => completed)
π€
some
Use this in a similar fashion to find
ππ½, but here we just get back true
or false
as long as at least one element in the array meets the predicate condition.
So, given the same data set as above ππ½: todos.some(({completed}
=> completed)will just give us back
true` because there at least some task has been completed β
.
Like find
, this one stops iterating once it finds a 'truthy' element.
every
Same as some
but makes sure that all the elements in the array meet the predicate. This one continues to iterate until it finds a 'fasle-y' element.
Again, given, that data set ππ½, todos.every(({completed}
=> completed) returns
false` b/c not every task has been completed β
.
The 'Main Roster' Superpowers
These next couple are used quite frequently - they are the 'workhorse' superpowers, getting a lot of things done.
These always return a new array.
map
We use this to perform some type of 'transform' on each and every element.
Each element is passed into a callback function. The function does something to each element, one at a time, creating a new array along the way.
Let's start by 'mapping' over a simple array of numbers, 'transforming' each one by doubling it. This example is taken from MDN.
const array1 = [1, 4, 9, 16];
// pass a function to map
const map1 = array1.map(x => x * 2);
console.log(map1);
// expected output: Array [2, 8, 18, 32]
array1
and map1
are totally separate arrays. array1
has not been mutated at all! ππ½
Now, let's do one with the todos
data from above ππ½. This time we'll 'complete' all of the tasks:
todos.map(todo => {
todo.completed = true;
return todo;
})
This will create a new array with all of the elements having: completed: true
.
However, if we check the original todos
, we will see that all of those 'todos' have been 'completed' also! ππ½
This is due to how JS treats objects. Essentially, the references in memory π§ are shared.
Going into the particulars of 'copy/value vs reference' is a topic for another day, but since React relies heavily on 'no mutations,' let's at least see the code to fix this. We'll use the object spread operator to create a 'fresh reference' to a totally new object before updating completed
to be true
.
todos.map(todo => {
const currentTodo = {...todo}
currentTodo.completed = true;
return currentTodo;
})
In case you need more info on ...
:
Now, the original array and all of the objects therein are not mutated, and we have a new array with the 'completed' data. π€
β οΈ You may be tempted to try object destructuring with map
. This is usually not desirable as once you perform the destructuring, you are limited to returning only those πs that you destructured.
For example π
π½ββοΈ:
todos.map(({completed}) => {
completed = true;
return completed;
})
would give us back: [ true, true, true, true, true ]
. So, generally, stay away from object destructuring when mapping.
Another common mistake is if you forget to explicitly return something from your 'map callback.' This will result in your 'mapped array' being full of undefined
.
todos.map(todo => {
const currentTodo = {...todo}
currentTodo.completed = true;
// No return? Returning undefined!
})
filter
This one works almost the same as find
in that it uses a predicate callback function. However, rather than just returning a single element, filter
will traverse the entire array, continuing to 'filter' 'truth-y' elements into a 'fresh new array.'
Let's filter out all of the even numbers (numbers that are divisible by 2 with no remainder) from an array of numbers:
const nums = [1, 4, 9, 16]
const evens = nums.filter(num => !(num % 2))
Again, we rely on a predicate callback function: num => !(num % 2)
- We pass in each 'num'
- Is
num % 2
'false-y'? (i.e. Is the remainder 0?) - Add the unary inverse operator:
!
to make it 'truthy' so that itreturn
s to our 'filtered array.'
Again, filter
return
s the element to a new array if the callback function return
s true
.
In this scenario, we ask, "Is it true that when we divide this number by 2 that the remainder is 'false-y'/0?"
Now, let's modify what we did for find
earlier and filter
our 'completed' tasks: todos.filter(({completed} => completed)
.
Just with one line of code, we have completed our task. We have all of the original data in todos
, and we have a separate 'filtered array' with just the 'completed' tasks. ππ½
map
- filter
βοΈ
These 'superpowers' can join forces. This time: //TODO: Filter out all 'incomplete' tasks and then assign those to 'Mary.'
Once again, using: todos
above ππ½:
todos.filter(({completed}) =>
// Return if it's NOT completed
!completed).
// Continue the chain by mapping over the 'incomplete tasks'
map(todo => {
const currentTodo = {...todo};
currentTodo.assignee = "Mary";
return currentTodo;
})
The results:
[
{
id: 1,
text: 'Take out the trash',
completed: false,
assignee: 'Mary'
},
{
id: 2,
text: 'Shop at Fresh Thyme',
completed: false,
assignee: 'Mary'
},
{
id: 5,
text: 'Litter Boxes Cleanup',
completed: false,
assignee: 'Mary'
}
]
The 'Leader' π of the Array Superpowers π¦Έπ½ββοΈ
One reason why reduce
is the most powerful of all of these is b/c, technically it can do the job of any of the ππ½. That being said, use the right tool for the right job - just b/c you can (and it's quite challenging, actually), doesn't mean that you should.
Use reduce
to...reduce your array into one single 'accumulated thing.'
One of the differences is that the reduce
callback function requires two parameters:
- An 'accumulator' to keep track of the 'ππ½ββοΈ total.'
- A 'current element' to keep track of...the current element in the array as we traverse.
For simplicity, we start with the most basic example, summing up numbers in an array:
const nums = [1, 2, 3, 4];
const total = nums.reduce((
// 'total' is initialized with the value of the first element -'1'
total,
// 'num' initializes as the 2nd element's value - '2'
num) => {
// Add 'total' to
total += num;
return total;
})
There is a lot more to reduce
that we can do. This example just scratches the surface. In the next post, we'll do some more 'practical'/intermediate things with map
, filter
and reduce
.
Posted on November 1, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.