Everyday JavaScript: Herding Data Using Objects and Arrays Interchangeably

We deal with a lot of data when building our JavaScript web applications. Whether it's a blog, ecommerce site, or a chat app - there's a lot of data we have to herd, and there's a few tasks we do over and over again: filtration, transformation, and selection.

The topic of data structures is vast, and this article will aim to just target some general knowledge that we can use in our everyday JavaScript lives. Performance, readability, and reusability are what we'll focus on.

We're not going to make any assumptions about where this data is coming from or where it's sitting, but we will assume that we have access to it whenever we need, and that the data has a defined structure.

The Object and Array Structure

Let's look at the same set of data, both as an Object and as an Array:

  
const objectOfBooks = {
    '111': {
        authorFirst: 'Jason',
        authorLast: 'Awbrey',
        id: '111',
        pages: 100,
        title: 'How to Do Things',
    },
    '222': {
        authorFirst: 'Jason',
        authorLast: 'Awbrey',
        id: '222',
        pages: 200,
        title: 'How NOT to Do Things',
    },
    '333': {
        authorFirst: 'Jason',
        authorLast: 'Awbrey',
        id: '333',
        pages: 300,
        title: 'Stuff: The Other Thing',
    },
}

const arrayOfBooks = [{
    authorFirst: 'Jason',
    authorLast: 'Awbrey',
    id: '111',
    pages: 100,
    title: 'How to Do Things',
},
{
    authorFirst: 'Jason',
    authorLast: 'Awbrey',
    id: '222',
    pages: 200,
    title: 'How NOT to Do Things',
},
{
    authorFirst: 'Jason',
    authorLast: 'Awbrey',
    id: '333',
    pages: 300,
    title: 'Stuff: The Other Thing',
}]
  

In the objectOfBooks we are using the id as the key, and each key is assigned to the value of a book. The arrayOfBooks houses a list of the same books. Both structures look pretty similar, and they contain the exact same data, so let's look at some key differences.

Filtration

Often we'll need to filter through data to only find the items we want based on a predicate. Perhaps we only want books that have more than 150 pages. Let's see what it could look like to do that with the Object structure:

  
const booksWithMoreThan150Pages = {}

for (const bookId in objectOfBooks) {
    if (objectOfBooks[bookId].pages > 150) { // the predicate
        booksWithMoreThan150Pages[bookId] = objectOfBooks[bookId]
    }
}
  

If we logged booksWithMoreThan150Pages to the console, we would see an object logged out that had the keys of '222', and '333'. Hurray! That wasn't so bad. Now, here's what the filtration could look like if it was an Array:

  
const booksWithMoreThan150Pages = arrayOfBooks.filter((book) => book.pages > 150)
  

Alright Array, you win this round.

Transformation

Often we'll want to only grab bits and pieces of our data. It's a common practice when we want to display data in a UI, like a list of blog posts or products. The transformation could involve formatting a date, creating a computed property, or shaking out data we don't need and delivering a structure of data to the UI that makes more sense. Let's grab only the title and compute the author's full name:

  
const booksWithOnlyTitleAndFullName = {}

for (const bookId in objectOfBooks) {
    booksWithOnlyTitleAndFullName[bookId] = {
        authorFullName: `${objectOfBooks[bookId].authorFirst} ${objectOfBooks[bookId].authorLast}`,
        title: objectOfBooks[bookId].title,
    }
}
  

Alright - let's see the Array beat that!

  
const booksWithOnlyTitleAndFullName = arrayOfBooks
    .map(({title, authorFirst, authorLast}) => ({
        authorFullName: `${authorFirst} ${authorLast}`,
        title,
    }))
  

I think we'll let the Array take this one too.

Tangentially, when we want to present a lot of items in a view in most front-end JavaScript frameworks like Angular, Vue, or React, we'll have an easier time using an Array structure - either because they won't loop over an Object, but they will loop over an Array, or the syntax is just easier to read with the Array. For a deeper dive on iteration check Chapter 21: Iterables and iterators, of Dr. Axel Rauschmayer's Exploring ES6.

Selection

Most data we'll deal with will have an ID. Let's see what it would look like to select a book with id 111. We'll give the Array a shot first this time:

  
const book = arrayOfBooks.find((book) => book.id === '111')
  

Hah! Marvelous! A one liner! Alright Object, let's see what ya got:

  
const book = objectOfBooks['111']
  

That was certainly more simple, even though the Array structure still wasn't too bad. The important consideration here is the big O notation of the selection. There's a computational cost associated with running a function, and looping over an Array versus selecting one item by reference in the Object structure takes more time/space to perform. That's where the Object structure here will shine.

You've got 1,000 items? With an Object that has keys as IDs for the items, it's a simple selection, and takes the same computational effort every time. With an Array, you'd have to possibly run a function for all 1,000 items. Using the Object structure for selection will give us an O(1) relationship, meaning the function will execute in the same amount of time/space every time. With the array, the relationship looks more like O(N), where N is the number of items. Computational execution will grow linearly along with the number of items in the case of the Array, so the Object wins at selection.

The Predicament

In Out of The Tarpit, the authors describe the fundamental complexity of software has to do with how we manage the state of our applications. State, in our case here, is a list of books. State could refer to lots of different things: is your computer on, is it AM or PM, is the user logged in. One of the main arguments of the paper is that **not** all state is **essential** for the application to function correctly, and complexity of development can be reduced by deriving information from already declared state.

State management is a real big topic in the front-end JavaScript community. Angular, React, Vue, and anyone else in the front-end JavaScript framework space, are all dealing with solving the same problem: managing state. In this article we're talking about how we can derive (filter/transform/select) information from our state (books). The structure of a web applications state and how we interact with it becomes paramount to the success of the application.

Where do we go from here? Do we keep our items in an Object structure, or an Array structure? We'll use them both, and instead of having duplicates of data, we should use the structures interchangeably, for whichever situation we're in. In essence, we'll have one structure, and derive from that what we need.

A Proposed Solution

For wherever the data actually lives, let's keep it housed as an Object with keys matching to the ID of the item. That way if a user goes to a page, and it's for "blog post #111", then we would know the ID they are looking for, and we can do the selection real fast. When it comes time to do a filter, transform, or other looping scenario, let's transmute the Object to an Array.

There are a few different ways to do this, and sometimes it's helpful to have utility functions on hand to transmute on the fly. Here are a couple examples:

  
function transmuteObjectToArray(allBooksObject) {
    return Object.keys(allBooksObject) // Gives us an array of all object keys
        .map(function (bookId) { // map will eventually return an Array structure
                return allBooksObject[bookId] // fill the array with books
            }
        )
}

function transmuteArrayToObject(allBooksArray) {
    return allBooksArray.reduce( // Loop over all the items in the Array, and returns an Object
        function (allBooksObject, currentBook) {
            Object.assign(allBooksObject, {[currentBook.id]: currentBook}) // Starts with {}, and adds {'bookId': Book}, for every book in the Array
            return allBooksObject
    }, {}) // this helps set the returned structure as an Object
}
  

Here are condensed versions, utilizing arrow functions and spread operators:

  
const transmuteObjectToArray = o => Object.keys(o).map(b => o[b])
const transmuteArrayToObject = a => a.reduce((acc, curr) => ({...acc, [curr.id]: curr}), {})

// implementation
transmuteObjectToArray(objectOfBooks)
transmuteArrayToObject(arrayOfBooks)
  

Have a different ID property other than "id"? No worries, just use something like this:

  
const transmuteArrayToObject = (a, id) => a.reduce((acc, curr) => ({...acc, [curr[id]]: curr}), {})

// implementation
transmuteArrayToObject(arrayOfBooks, '_id')
  

Putting It All Together

Using our new utilities, let's revisit our different scenarios knowing that the source of our data will be in an Object structure.

  
// filter
const booksWithMoreThan150Pages = transmuteObjectToArray(objectOfBooks)
    .filter(book => book.pages >= 150)

// transform
const booksWithOnlyTitleAndFullName = transmuteObjectToArray(objectOfBooks)
    .map(({title, authorFirst, authorLast}) => ({
        authorFullName: `${authorFirst} ${authorLast}`,
        title,
    }))

// selection
const book = objectOfBooks['111']
  

Now we have a clear and concise way of switching our Object into an Array. Sometimes we'll want to consume some data from an API and it will be in the form of an Array. We can change it to an Object using our new utility. Other times we might need to switch back and forth, like this:

  
function transformBooks(books) {
    const transformed = transmuteObjectToArray(books)
        .filter(book => book.pages >= 150)
        .map(({
            authorFirst, 
            authorLast,
            id,
            title, 
        }) => ({
            authorFullName: `${authorFirst} ${authorLast}`,
            id,
            title,
        }))
    return transmuteArrayToObject(transformed)
}

// implementaiton
const books = transformBooks(objectOfBooks)
  

All this helps us with performance and readability. Next let's take a look at how we can extend this concept further to take advantage of reusability.

Composing the Bits and Pieces

We'll use a higher order function technique to help us compose some reusable code, making it easier to filter, transform, and select our data.

Reusable Filters

  
// setup for reusable filters
const filterBooksByPredicate = books => predicate => transmuteObjectToArray(books).filter(predicate)
const allBookData = filterBooksByPredicate(objectOfBooks)
const filterByPageMin = pageMin => allBookData((book) => book.pages >= pageMin)
const filterByAuthorLastName = lastName => allBookData((book) => book.authorLast === lastName)

// implementation
const getBooksWith99OrMorePages = filterByPageMin(99)
const getBooksWith123OrMorePages = filterByPageMin(123)
const getBooksByAwbrey = filterByAuthorLastName('Awbrey')
const getBooksByYerbwa = filterByAuthorLastName('Yerbwa')
  

Reusable Transforms

  
const getBooksWithTheseProperties = (books, ...properties) => transmuteObjectToArray(books) // only had to transmute once!
        .map((book) => properties
            .reduce((newBook, property) => ({
                ...newBook, // now we have something reuseable
                [property]: book[property], // we can ask for only the properties of the book we want
            }), {})
        )

// implementation
const getBooksWithTitleAndPages = getBooksWithTheseProperties(objectOfBooks, 'title', 'pages')
const getBooksWithTitleAndFirstAndLastName = getBooksWithTheseProperties(objectOfBooks, 'title', 'authorFirst', 'authorLast')
const getBooksWithLastAndId = getBooksWithTheseProperties(objectOfBooks, 'authorLast', 'id')
const getBooksWithTitleAndFullName = getBooksWithTitleAndFirstAndLastName
            .map(book => ({
                authorFullName: `${book.authorFirst} ${book.authorLast}`,
                title: book.title,
            })
        )
  

Reusable Selectors

  
const getBookById = books => id => books[id]
const getBook = getBookById(objectOfBooks)

// imeplementation
const book111 = getBook('111')
const book222 = getBook('222')
const book333 = getBook('333')
  

Conclusion

We can write data structures and interact with them in a way that’s readable, reusable, and performant. This sort of work can take a lot of effort up front, but pays off. It helps us, and it helps the teams we work with too.