How to Transform Any Data Structure with Reduce

Last reviewed in June 2019

Fold is a common higher-order function widely used in functional programming. In Javascript, we have an analogous function called reduce.

While not specific to Ember, reduce is an amazingly helpful tool. It is so versatile that it allows us to transform data structures in virtually any way we want.

diagram

This is the anatomy of a reduce:

array.reduce((acc, value, index, array) => {
  // ...
  return acc;
}, initialValue);

The supplied function will be called once per element in the array:

  • value is the content of the current element of the array, and index… well, its index
  • acc is what I call the “accumulator”. It is the object that holds state across iterations. For the first element of the array, acc will take the value supplied in initialValue. For the remaining elements, it will take the value returned in the previous invocation of the function.

Understanding with examples

It can sum an array (from Array to Number):

[1, 2, 3].reduce((acc, value) => {
  return acc + value;
}, 0);
// => 6

or find a max:

[1, 2, 7, 3].reduce((acc, value) => {
  if (acc < value) acc = value;
  return acc;
}, -Infinity);
// => 7

It can flatten nested objects (from Array or Object to Array):

const nested = {
  id: 1,
  children: [
    { id: 2 },
    { id: 3,
      children: [{ id: 5 }, { id: 6 }]
    },
    { id: 4 }
  ]
}

const flatten = (obj) => {
  const array = Array.isArray(obj) ? obj : [obj];
  return array.reduce((acc, value) => {
    acc.push(value);
    if (value.children) {
      acc = acc.concat(flatten(value.children));
      delete value.children;
    }
    return acc;
  }, []);
}

flatten(nested);
// => [ { id: 1 }, { id: 2 }, { id: 3 }, { id: 5 }, { id: 6 }, { id: 4 } ]

It can implement map:

const map = (array, fn) => {
  return array.reduce((acc, value) => {
    value = fn.call(this, value);
    acc.push(value);
    return acc;
  }, []);
}

// add 1 to each number
map([1, 2], n => n + 1);
// => [2, 3]

…or filter:

const filter = (array, fn) => {
  return array.reduce((acc, value) => {
    const ok = fn.call(this, value);
    if (!!ok) acc.push(value);
    return acc;
  }, []);
}

// return only even numbers
filter([1, 2, 5, 7, 8, 10, 104, 1189], n => n % 2 === 0);
// => [ 2, 8, 10, 104 ]

It can join strings:

["hello", "this", "is", "awesome"].reduce((acc, value) => {
  if (acc) acc = acc.concat(" ");
  acc = acc.concat(value);
  return acc;
}, "");
// => 'hello this is awesome'

And even call promises serially:

const models = [model1, model2, model3];

models.reduce((previous, model) => {
  return previous.then(() => model.save());
}, RSVP.resolve());

In Ember

Say we have a template displaying a list of spare parts (each with its name, sku and amountInStock).

In the component (or controller) that backs that template we can define totalAmountInStock:

totalAmountInStock: computed('parts.@each.amountInStock', function() {
  return this.parts.reduce((acc, value) => {
    return acc + value.amountInStock;
  }, 0);
});

// and then {{totalAmountInStock}} in the template!

The total amount gets effortlessly updated!

(Yes, I know this can be done with computed.sum… but hey, I needed an easy example!)

To sum up, no pun intended, whenever you need to transform data (even from one data type to another) there is one tool that will always come handy: that is reduce!

Enjoyed this article? Join Snacks!

Snacks is the best of Ember Octane in a highly digestible monthly newsletter. (No spam. EVER.)