Editing the accumulator object in Reduce function in JS?

99 Views Asked by At

I have this code snippet I'm using to group by a key field.

It's using the reduce function and I understand the code except the part where the result or accumulator of the function is being edited.

If I were to write this code, I'd have an external empty array to push my results to and not edit the object in the function.

The code works as intended but I was wondering if this is common practice.

function groupBy(array, key) {
  return array.reduce((result, currentItem) => {
    const groupByKey = currentItem[key];

    // Create a new array for the key if it doesn't exist in the result
    if (!result[groupByKey]) {
      result[groupByKey] = [];
    }

    // Push the current item to the array of the corresponding key
    result[groupByKey].push(currentItem);

    return result;
  }, {});
}

// Example usage:
const data = [
  { id: 1, category: 'A' },
  { id: 2, category: 'B' },
  { id: 3, category: 'A' },
  { id: 4, category: 'C' },
  { id: 5, category: 'B' },
];
4

There are 4 best solutions below

0
sp00m On

I believe it's a good practice to try keeping your functions pure indeed, especially with these functional patterns, although in this case, it isn't much of an issue, given the initial value of the accumulator is {}, an object literal that has been built specifically to be passed to that reduce.

It would be an issue if the accumulator were used beforehand/afterhand, for example:

const data = {};

// some logic around data...

const reduced = array.reduce((acc, e) => { /* mutating acc */ }, data);

// data has changed unexpectedly

In a nutshell, your current impl is acceptable IMO, but I would have written it without mutating result either personally.

2
Krzysztof Krzeszewski On

Using modern javascript, you would probably use a native method for that, instead of creating your own grouper.

const data = [
  { id: 1, category: 'A' },
  { id: 2, category: 'B' },
  { id: 3, category: 'A' },
  { id: 4, category: 'C' },
  { id: 5, category: 'B' },
];

console.log(Object.groupBy(data, ({category}) => category));

However it has limited availability in some of the browsers like samsung internet or safari

0
Keyboard Corporation On

That approach is very efficient because it only requires a single pass through the array, and it avoids creating unnecessary intermediate arrays. It also makes the code more concise and easier to read, because you don't need to manually manage an external array.

So, while it's possible to create an external array and push results to it, doing so would likely be less efficient and more verbose than modifying the accumulator object directly.

0
Alexander Nenashev On

As you could know reduce() is a functional method of Array(). In a functional method you call it with parameters and get some data without mutating either any data passed to the function or data outside the function. Such methods are called pure.

The problem here is also variable scoping I think. For example you could have actually several objects in an accumulator: {arr: [], map: {}} - where arr is an actual value we want to return and use, and map - a helper object. If you put them as external scope variables:

  1. You don't need reduce() anymore, use forEach()
  2. BOOM, you pollute your code with arr and map variables which could clash with variables with the same name
  3. To avoid that you need to put everything in a block to seal you operation's scope.
  4. BOOM, since you need to use arr later, and it should have a more definite name you should put it outside the block.
  5. Overall you code becomes bloated, instead of using reduce() and its function scope to seal everything:
const result = [];
{
    const map = {};
    array.forEach(...);
}

So using reduce() is a quite nice shortcut that could be even a one-liner.

If you need many helper accumulators I would suggest to try:

const data = [
  { id: 1, category: 'A' },
  { id: 2, category: 'B' },
  { id: 3, category: 'A' },
  { id: 4, category: 'C' },
  { id: 5, category: 'B' },
];

const groupedArray = data.reduce((map => (r, item) => 
  ((map[item.category] ??= r[r.length] = {ids:[], category: item.category}).ids.push(item.id), r))({}), []);
  
console.log(groupedArray);