DEV Community

Garrick Crouch
Garrick Crouch

Posted on

Creating Data Structures with Array.reduce()

I recently saw an older youtube video on using array.reduce to build data structures on the fly in ways that you may find surprising or unintuitive. Normally we always think of reduce when it comes to doing math on array elements or something similar, and while that is a great use case, lets explore some of the more unique ways leverage this array method.

Create An Object From An Array

To do this you could use any old loop, but lets say you need to build an object of objects, with the properties equal to one of the objects property values, eg.

// this is the data we have...
const data = [
  {
    id: 1,
    name: 'New Post',
    author: 'Jeff',
    date: '2021-05-01'
  },
  {
    id: 2,
    name: 'Newer Post',
    author: 'Sabrina',
    date: '2021-05-02'
  },
  {
    id: 3,
    name: 'Newest Post',
    author: 'Mike',
    date: '2021-05-02'
  },
  {
    id: 4,
    name: 'Fourth Post',
    author: 'Mike',
    date: '2021-03-02'
  },
  {
    id: 5,
    name: 'Fifth Post',
    author: 'Sabrina',
    date: '2021-08-09'
  }
];

// this is the structure we want...
const authors = {
  jeff: {
    posts: [
      {
        id: 1,
        title: 'Post Name',
        created_at: '2021-05-01'
      }
    ]
  },
  sabrina: {
    posts: [ ...posts ]
  },
  mike: {
    posts: [ ...posts ]
  },
}
Enter fullscreen mode Exit fullscreen mode

Basically we want build an object containing author objects that each contain an array of any posts they've written. A map won't do because we don't really want to return an array of course (contrived on purpose for the example) and we would like to easily aggregate them into the appropriate arrays keyed by the name. Also the specification says we should rename the date to created_at and name to title.

So how could we reduce this array to the data structure specified in a functional way and it make sense to the reader of our code?

Remember that array.reduce will return any value you want it to...aha...so we want to return an object.

reduce((previousValue, currentValue) => { ... }, initialValue)
Enter fullscreen mode Exit fullscreen mode

This above is the function we'll use. Notice the initialValue argument. That'll set the stage for our returned value.

Let's Reduce

(data || []).reduce((acc, curr) => ({}), {});
Enter fullscreen mode Exit fullscreen mode

This is our basic setup. We'll pass acc or the accumulated value and the curr or current array element into the callback, returning an expression, which is an object literal. Our default value you may notice is an empty object.

const result = (data || []).reduce((acc, curr) => ({
  ...acc,
  [curr?.author?.toLowerCase()]: {
    ...acc[curr?.author?.toLowerCase()],
    posts: [
      ...(acc[curr?.author?.toLowerCase()]?.posts || []),
      {
        id: curr?.id,
        title: curr?.name,
        created_at: curr?.date
      }
    ]
  }
}), {});
Enter fullscreen mode Exit fullscreen mode

This is our workhorse above. We'll step through each stage of working with the data. It's done in a functional way meaning we're copying data, never overwriting it.

First, we spread the value of acc into the object that we're returning
const result = data.reduce((acc, curr) => ({
  ...acc,
  // more stuffs
}), {});
Enter fullscreen mode Exit fullscreen mode
Second, we'll use the computed value to set our property name of an author
const result = data.reduce((acc, curr) => ({
  ...acc,
  [curr?.author?.toLowerCase()]: {
    // more stuffs
  }
}), {});
Enter fullscreen mode Exit fullscreen mode

This way it ensures we're preserving any objects that don't match the computed property name in the carry. We use the toLowerCase bc the spec says it wants lowercase author names as the object property.

Third, we'll set and spread on the posts property of a computed name author object
const result = data.reduce((acc, curr) => ({
  ...acc,
  [curr?.author?.toLowerCase()]: {
    ...acc[curr?.author?.toLowerCase()],
    posts: [
     // we'll use a short circuit since the posts property won't e 
     // exist on the first of any given author, just spread an 
     // empty array
      ...(acc[curr?.author?.toLowerCase()]?.posts || []),
     // add our object with the specified data mapping
      {
        id: curr?.id,
        title: curr?.name,
        created_at: curr?.date
      }
    ]
  }
}), {});
Enter fullscreen mode Exit fullscreen mode
Success

If we serialize the result and pretty print it we'd get....

{
    "jeff": {
        "posts": [
            {
                "id": 1,
                "title": "New Post",
                "created_at": "2021-05-01"
            }
        ]
    },
    "sabrina": {
        "posts": [
            {
                "id": 2,
                "title": "Newer Post",
                "created_at": "2021-05-02"
            },
            {
                "id": 5,
                "title": "Fifth Post",
                "created_at": "2021-08-09"
            }
        ]
    },
    "mike": {
        "posts": [
            {
                "id": 3,
                "title": "Newest Post",
                "created_at": "2021-05-02"
            },
            {
                "id": 4,
                "title": "Fourth Post",
                "created_at": "2021-03-02"
            }
        ]
    }
}
Enter fullscreen mode Exit fullscreen mode

Please leave me any thoughts on optimization or better ways to accomplish the given task. The primary focus of this is to get people thinking about array.reduce in interesting ways but I always enjoy learning new or better ways to do stuff.

Discussion (0)