DEV Community

Manav Misra
Manav Misra

Posted on

Array 'Superpowers' 🦸🏾

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
})
Enter fullscreen mode Exit fullscreen mode

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 returns based on upon coercion to either true or false.

In other words:

  1. Iterate over todos, passing in each 'todo' one at a time.
  2. Have a look πŸ‘€ at todo.completed. If it's true, return this todo. 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 backtrue` 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]
Enter fullscreen mode Exit fullscreen mode

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;
})
Enter fullscreen mode Exit fullscreen mode

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;
})
Enter fullscreen mode Exit fullscreen mode

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;
})
Enter fullscreen mode Exit fullscreen mode

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!
})
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

Again, we rely on a predicate callback function: num => !(num % 2)

  1. We pass in each 'num'
  2. Is num % 2 'false-y'? (i.e. Is the remainder 0?)
  3. Add the unary inverse operator: ! to make it 'truthy' so that it returns to our 'filtered array.'

Again, filter returns the element to a new array if the callback function returns 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;
})
Enter fullscreen mode Exit fullscreen mode

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'
  }
]
Enter fullscreen mode Exit fullscreen mode

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:

  1. An 'accumulator' to keep track of the 'πŸƒπŸ½β€β™‚οΈ total.'
  2. 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;
})
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)