DEV Community

Chris Held for Headway

Posted on

Explaining Javascript Reduce

The javascript reduce function is a great way to keep your code immutable to prevent (or "reduce", har har) surprises later on, but the syntax is kind of arcane looking and most of the examples online are for extremely simple cases like aggregating numbers.

Let's say we have an array of objects that need its ids mapped to an array:

const selected = rows.map(row => row.id)

Now let's add another wrinkle. These rows can have any number of children, and their ids have to be added as well (let's save children of children and recursion for another post). One thing we could do is keep the original selected array and loop through rows to push children ids onto it:

let selected = [];
rows.forEach(row => {
  selected.push(row.id);
  if (row.children) {
    row.children.forEach(child => {
      selected.push(child.id);
    });
  }
})

That'll work, but we've got a nested loop there, and mutating the array could introduce bugs. Another option is to use javascript's reduce function:

      const selected = [
        ...rows.map(row => row.id),
        ...rows.reduce((accum, row) => (
            row.children ? [...accum, ...row.children.map(c => c.id)] : accum
          ), 
          []
        )
      ];

There's a lot going on here, so let’s step through:

      const selected = [
        ...rows.map(row => row.id)

This part is pretty straightforward, we create a new array and use the spread operator to put our original array there (since we still want all of the parent rows):

        ...rows.reduce((accum, row) => (
            row.children ? [...accum, ...row.children.map(c => c.id)] : accum
          ), 
          []
        )

This line is where we make use of the reduce function. Reduce loops over our array of rows and calls a function for each one. The function has two arguments, the current row object and an accum or “accumulator” parameter that is whatever was passed from the last invocation of the function.

In our function we’re checking if this row has children, and if it does we’re using the spread operator again to return a new array containing what we’ve accumulated thus far plus the ids of any of this row’s children. If there are no children, we just return what we were passed in as the first argument, since we want to keep potentially adding to that value.

Something that is often overlooked is the last argument passed to the reduce function is the initial state of the accumulator (an empty array in our above example). If it’s not initialized it starts as undefined, which can result in some errors or bizarre behaviors.

One last important point about reduce is that it can be hard to read and process mentally, especially when either a new person is taking over a feature / codebase or you're coming back to it after a long break. In the case here reduce solves our problem without causing too much mental overhead, but sometimes opting for another solution will make life much easier for future you.

Top comments (0)