Don't search an array, look up instead

By Simon Smithin JavaScript

4 min read

Let us imagine that I have a collection of data that needs to be searched through quite often, for example an array of users:

const users = [
  {
    name: 'Vinnie Coffey',
    id: 345623,
  },
  {
    name: 'Tiana Phelps',
    id: 768547,
  },
  {
    name: 'Zackary Solomon',
    id: 129865,
  },
  {
    name: 'Salma Berg',
    id: 198576,
  },
  // --  and so on with many more objects
]

We can pretend that in real life this array of users could be quite large and if I need to look up a user frequently it would be inefficient to continually loop through it with something like [].find(). The user I want could be near the end each time!

One way to get round this is to create an object or Map that uses a unique identifier as the key and allows me to find items without any iteration.

To be clear, I'm thinking of something like this:

const users = {
  129865: {
    name: 'Zackary Solomon',
    id: 129865,
  },
  198576: {
    name: 'Salma Berg',
    id: 198576,
  },
  // -- etc
}

const getUserById = (id) => {
  return users[id]
}

Now, instead of sifting through the users each time, I can access a user directly if I know their id:

const salma = getUserById('198576')

Nice. This works much better for frequent lookups. Additionally, the helper function makes it clear what we're doing and keeps any future refactoring in one place.

Transforming the array to an object

A good question would be, how do we convert the array to the lookup version above? Thankfully, it requires minimal effort and it can also be packaged up into a function:

const transformArrayToLookup = (lookupKey, array) => {
  const transformed = {}
  array.forEach((item) => {
    const key = item[lookupKey]
    transformed[key] = item
  })
  return transformed
}

const lookup = transformArrayToLookup('id', users)

// here we return the user by accessing it with the id, instead
// of performing a `find`
const getUserById = (id) => {
  return lookup[id]
}

So, we loop through the array once and return a new object with the chosen key (in this case id) as the property we use to 'lookup' a user.

This could be made a little less verbose with a bit of help from reduce, but as I grow older and wiser I get more confident that reduce is just not worth the overhead for people trying to understand it.

Is reduce() bad? - HTTP 203

Okay, so are we done here? Technically yes, this works just fine. But, before you leave I thought we could try and make it even neater.

Using the brand new Object.groupBy

In case you didn't know, both Object.groupBy and Map.groupBy have recently made it to TC39 Stage 4 proposal and this indicates that:

"the addition is ready for inclusion in the formal ECMAScript standard"

It's actually ready for use now in everything except the very latest Safari:

image of groupby can i use table

What does it do?

To put it simply, it's designed to group objects by a common key. I'll use the example from the MDN page to demonstrate

const inventory = [
  {name: 'asparagus', type: 'vegetables', quantity: 5},
  {name: 'bananas', type: 'fruit', quantity: 0},
  {name: 'goat', type: 'meat', quantity: 23},
  {name: 'cherries', type: 'fruit', quantity: 5},
  {name: 'fish', type: 'meat', quantity: 22},
]

const result = Object.groupBy(inventory, ({type}) => type)

/* Result is:
{
  vegetables: [
    { name: 'asparagus', type: 'vegetables', quantity: 5 },
  ],
  fruit: [
    { name: "bananas", type: "fruit", quantity: 0 },
    { name: "cherries", type: "fruit", quantity: 5 }
  ],
  meat: [
    { name: "goat", type: "meat", quantity: 23 },
    { name: "fish", type: "meat", quantity: 22 }
  ]
}
*/

If you look closely you can see it almost does what we created with the transformArrayToLookup, but the crucial difference is the value of each key is an array, not an object. Luckily we hid our implementation for retrieving a user inside a function, because we're smart like that. It will be a quick change to return the object from the array as we're only ever expecting it to have a single item.

A bit of refactoring

Let's make some changes then, first by making use of groupBy and then ensuring that we get an object back, and not an array:

const transformArrayToLookup = (lookupKey, array) => {
  return Object.groupBy(array, (item) => {
    return item[lookupKey]
  })
}

const lookup = transformArrayToLookup('id', users)
const getUserById = (id) => {
  return lookup[id][0] // <--- this ensures we get the object as before
}

// works!
const salma = getUserById('198576')

This allows a terser approach to creating the lookup (compared to a forEach or reduce) but it could be argued that it sacrifices some readability if other engineers are not familiar with groupBy. It also relies on getUserById returning the first index which is not overly ergonomic.

I'll let you decide which approach you prefer.

Resources

Comments