DEV Community

Samuel Rouse
Samuel Rouse

Posted on

Arrays: Reduce - Make Something

When you have a set of data you want to transform into something else, like an object or a sum, you are reducing.

Method Returns One-for-one Runs for All
.reduce((accumulator, value, index, array) => *, optionalInitial) * No Yes

The .reduce() function takes a reducer – a function that collects the end result as it iterates over each entry. Unlike most other array prototype methods, this includes an additional parameter, the accumulator, or the value that is passed from each iteration to the next. This, well, accumulates the new result. An optional initial value is passed to the .reduce() function as well. If it is not, we receive the first value as the accumulator and save one iteration.

In some examples below we will abbreviate the accumulator to acc for brevity, but the meaning is the same.

Code Comparison

const data = [1, 2, 3, 4];
Enter fullscreen mode Exit fullscreen mode
// Older Imperative way - for loop
function sum(data) {
  const result = 0;

  for (let i = 0; i < data.length; i += 1) {
    result += data[i];
  }

  return result;
}
Enter fullscreen mode Exit fullscreen mode
// Newer Imperative way - for..of loop
function sum(data) {
  const result = 0;

  for (let value of data) {
    result += value; 
  }

  return result;
}
Enter fullscreen mode Exit fullscreen mode
// New Declarative way - .reduce()
const sum = (data) => data
  .reduce((acc, value) => acc + value, 0);

// Or, saving that first iteration
const sum = (data) => data.reduce((acc, value) => acc + value);
Enter fullscreen mode Exit fullscreen mode

Use Cases

Sums

The simplest and most common example of .reduce() is performing arithmetic on an array of numbers.

const data = [1, 2, 3, 4, 5, 6];
const sum = data.reduce((acc, value) => acc + value);

sum; // 21
Enter fullscreen mode Exit fullscreen mode

In this case we didn't provide the optional initial value, so the first array value was passed as the accumulator, but it is roughly equivalent to the following:

const sum = data.reduce((accumulator, value) => {
  return accumulator + value;
}, 0);
Enter fullscreen mode Exit fullscreen mode

I used a block body on the arrow function for clarity. It works without it, but the comma separating the initial value can be more confusing to quickly read when we use an expression body:

const sum = data.reduce((acc, value) => acc + value, 0);
Enter fullscreen mode Exit fullscreen mode

Compounding

Another simple arithmetic action is multiplication or compounding. If you've ever needed to calculate "take an extra 20% off the 75% sale!", .reduce() can be helpful.

const calculatePrice = (discounts, originalPrice) => discounts
  .reduce(
    (accumulator, discount) => accumulator * (1 - discount),
    originalPrice
  );

calculatePrice([.5, .3, .1], 1000); // 315
calculatePrice([.75, .2], 150); // 30
Enter fullscreen mode Exit fullscreen mode

Objects, But Carefully!

Creating an object or hash from an array of elements, often called a list or collection, is pretty straightforward. While there are many ways this can happen, be aware that some have serious performance impacts.

// Mutate a new object to add properties.
const result = list.reduce((accumulator, entry) => {
  accumulator[entry.key] = entry.value;
  return accumulator;
}, {});

// DON'T DO THIS: Substantial performance impact
// Spread to a new object each time.
const result = list.reduce((accumulator, entry) => ({
  ...accumulator,
  [entry.key]: entry.value,
}), {}); 
Enter fullscreen mode Exit fullscreen mode

There are also alternatives like Object.fromEntries() that are designed to efficiently turn an array into an object.

Once you exceed a few dozen keys, creating a new object on each iteration can have serious performance impact. We eliminated several hundred milliseconds of processing time in one application by switching to Object.fromEntries().

Special Examples

Reduce is somewhat special in that we can also reproduce most of the other array methods with it. It is not as efficient or as clear to do it this way, but we can, so let's see what that looks like.

Filter

const data = [1, 2, 3, 4, 5, 6];

// Filter with reduce
const oddNumbers = data.reduce((accumulator, value) => {
  if (isOdd(value)) {
    accumulator.push(value);
  }
  return accumulator;
}, []);

oddNumbers; // [ 1, 3, 5 ]
Enter fullscreen mode Exit fullscreen mode

Some

const data = [1, 2, 3, 4, 5, 6];
const isOdd = (val) => !!(val % 2);

// Some with reduce
const someOdd = data.reduce((accumulator, value) => {
  // We have to run each cycle, no matter what
  //  but we can skip the "work" if we know the answer
  return accumulator || isOdd(value);
}, false);

someOdd; // true
Enter fullscreen mode Exit fullscreen mode

Every

// Every with reduce - Starts assuming true
const everyOdd = data.reduce((accumulator, value) => {
  // Once it is false, we know "not every"
  //  so we can "skip" the work.
  if (!accumulator) {
    return accumulator;
  }
  return isOdd(value);
}, true);

everyOdd; // false
Enter fullscreen mode Exit fullscreen mode

State Changes

If you use Redux or the useReducer() hook, the idea is almost the same. Instead of reducing data, we are reducing actions. What we've referred to as the accumulator is accumulating the state, and the value is whatever action is being performed to update the state. We can take a group of actions and allow each one to make changes.

const updateState = (currentState, setOfActions) => {
  return setOfActions.reduce((state, action) => {
    // Reducers often use switch for many options
    switch (action.type) {
      case 'counter/increment':
      return {
        ...state,
        counter: state.counter + 1,
      };
      case 'counter/decrement':
      return {
        ...state,
        counter: state.counter - 1,
      };
      case 'message/set':
      return {
        ...state,
        message: action.payload,
      };
    }
    // No match, return current;
    return state;
  }, currentState);
};

updateState({ counter: 0 }, [{ type: 'counter/increment' }])
// { counter: 1 }

// Perform multiple updates
updateState({ counter: 0 }, [
  { type: 'counter/increment' },
  { type: 'counter/increment' },
  { type: 'counter/decrement' },
  { type: 'message/set', payload: 'Hello!' }
]);
// { counter: 1, message: 'Hello!' }
Enter fullscreen mode Exit fullscreen mode

Flow

Data and actions aren't the only things we can reduce. We can operate on functions as well! This is the basic design of the flow function used with a functional style of programming.

const flow = (functions) => (input) => functions
  .reduce((lastInput, fn) => fn(lastInput), input);

const handshakes = flow([
  (x) => x * (x - 1),
  (x) => x / 2 ,
]);

handshakes(4); // 6
Enter fullscreen mode Exit fullscreen mode

Taking an initial input, we perform the action and pass on the result to the next function, allowing us to reduce a set of functions into one operation. It may help to think of this as a pipeline.

For this simple example we broke the handshake problem into two steps to demonstrate how flow works.

Promises

Similar to flow, but with asynchronous functions we can't directly take the output to pass to the next function. Using promise chains we can wait for the result of one before continuing to the next. This is sometimes called serializing the asynchronous operations.

const asyncFlow = (functions) => (input) => functions.reduce(
  // Using .then() passes the last value to the function 
  (lastPromise, fn) => lastPromise.then(fn),
  // Start with resolve to guarantee a promise chain
  Promise.resolve(input),
);
Enter fullscreen mode Exit fullscreen mode

While necessary at times, be aware that alternatives like Promise.all() exist when requests can happen in parallel.

This pattern works like a regular promise chain and assumes each operation will succeed. If any one fails or returns a rejected promise, the rest of the operations will not be executed.

Conclusion

Reduce is perhaps the most flexible of the array methods because it operates on every entry in the array and it has a return value, unlike .forEach(). There may be more efficient methods for some uses, but if you need to transform an array of data, actions, or functions, .reduce() is a great place to start.

Sentry blog image

How to reduce TTFB

In the past few years in the web dev world, we’ve seen a significant push towards rendering our websites on the server. Doing so is better for SEO and performs better on low-powered devices, but one thing we had to sacrifice is TTFB.

In this article, we’ll see how we can identify what makes our TTFB high so we can fix it.

Read more

Top comments (0)

AWS Q Developer image

Your AI Code Assistant

Automate your code reviews. Catch bugs before your coworkers. Fix security issues in your code. Built to handle large projects, Amazon Q Developer works alongside you from idea to production code.

Get started free in your IDE

👋 Kindness is contagious

Engage with a wealth of insights in this thoughtful article, valued within the supportive DEV Community. Coders of every background are welcome to join in and add to our collective wisdom.

A sincere "thank you" often brightens someone’s day. Share your gratitude in the comments below!

On DEV, the act of sharing knowledge eases our journey and fortifies our community ties. Found value in this? A quick thank you to the author can make a significant impact.

Okay