Using Array.reduce With Objects
Tags: javascript
Published: 2022-01-04 14:23:48

This article will demonstrate a few ways that you can use JavaScript's `Array.reduce` method to transform objects.

## An Array.reduce primer

Let's take a quick look at how `Array.reduce` works. You can skip this if you're already familiar with it.

`Array.reduce` reduces an array down to a single value. The resulting value can be of any type &mdash; it does not have to be an array. This is one way in which `Array.reduce` differs from other array methods like `map` and `filter`. Here's a reduce statement that returns the sum of an array of numbers:

#### Example 1:

	const numbers = [1, 2, 3, 4, 5];
	const sum = numbers.reduce((next, number) => {
	  return next + number;
	}, 0);

`Array.reduce` accepts two arguments:

1. A callback function that is executed for each item in the array in series and receives the following parameters:
	* The _accumulator_ (called `next` in the example), which is the working value. In the first iteration the accumulator is set to the initial value (`0`). For all subsequent iterations it's the value returned by the previous iteration.
	* The current array item (`number` in the example).
	* The current array index (not used in the example).
	* The array that is being reduced (not used in the example).
2. The initial value for the accumulator. In _Example 1_, the initial value is `0`.

The reduce statement in `Example 1` will execute the callback function five times with the following values:

1. **Accumulator (`next`):** `0` (the initial value); **Value (`number`):** `1`; **Returns:** `1`;
2. **Accumulator:** `1`; **Value:** `2`; **Returns:** `3`;
3. **Accumulator:** `3`; **Value:** `3`; **Returns:** `6`;
4. **Accumulator:** `6`; **Value:** `4`; **Returns:** `10`;
5. **Accumulator:** `10`; **Value:** `5`; **Returns:** `15`;

The final value of `sum` will be `15`.

## Array.reduce applied to objects

Remember that `Array.reduce` can use initial and return values of any type, which makes it very flexible. Let's explore how we can use it to perform some common tasks with plain objects.

### 1. Converting an array of objects to a single object keyed by id:

Developers frequently have to look up a value in one array using a value from another array. Consider the following hypothetical example where we have an array of objects representing users and another array representing profiles. Each user has an `id` property and each profile has a `userId` property. We need to match each user with their profile, where `user.id` equals `profile.userId`. A basic implementation of this is shown in _Example 2_.

#### Example 2:

	const users = [
	  { id: 1, email: 'dcontreras@email.tld' },
	  { id: 2, email: 'afeher@email.tld' },
	  { id: 3, email: 'odj@email.tld' },
	];

	const profiles = [
	  { userId: 1, firstName: 'Danielle', lastName: 'Contreras' },
	  { userId: 2, firstName: 'Alfredas', lastName: 'Fehér' },
	  { userId: 3, firstName: 'Orpheus', lastName: 'De Jong' },
	];

	const usersWithProfiles = users.map((user) => {
	  const profile = profiles.find((profile) => (user.id === profile.userId));
	  return { ...user, profile };
	});

	// usersWithProfiles:
	// [
	//   { id: 1, email: 'dcontreras@email.tld', profile: { userId: 1, firstName: 'Danielle', lastName: 'Contreras' } },
	//   { id: 2, email: 'afeher@email.tld', profile: { userId: 2, firstName: 'Alfredas', lastName: 'Fehér' } },
	//   { id: 3, email: 'odj@email.tld', profile: { userId: 3, firstName: 'Orpheus', lastName: 'De Jong' } },
	// ]

The problem with _Example 2_ is that it uses `Array.find` inside `Array.map` which is inefficient. This might not matter for the short arrays in the example, but it will become more of a problem when working with longer arrays. The longer the `profiles` array is, the longer the potential lookup time will be for any given profile. We can solve this problem by transforming the `profiles` array into an object beforehand, using the `userId` property as the key:

#### Example 3:

	const users = [
	  { id: 1, email: 'dcontreras@email.tld' },
	  { id: 2, email: 'afeher@email.tld' },
	  { id: 3, email: 'odj@email.tld' },
	];

	const profiles = [
	  { userId: 1, firstName: 'Danielle', lastName: 'Contreras' },
	  { userId: 2, firstName: 'Alfredas', lastName: 'Fehér' },
	  { userId: 3, firstName: 'Orpheus', lastName: 'De Jong' },
	];

	// Transform the profiles into an object keyed by the userId:
	const profilesByUserId = profiles.reduce((next, profile) => {
	  const { userId } = profile;
	  return { ...next, [userId]: profile };
	}, {});

	// profilesByUserId:
	// {
	//   1: { userId: 1, firstName: 'Danielle', lastName: 'Contreras' },
	//   2: { userId: 2, firstName: 'Alfredas', lastName: 'Fehér' },
	//   3: { userId: 3, firstName: 'Orpheus', lastName: 'De Jong' },
	// }

	// Look up the profiles by id:
	const usersWithProfiles = users.map((user) => {
	  return { ...user, profile: profilesByUserId[user.id] };
	});

	// usersWithProfiles:
	// [
	//   { id: 1, email: 'dcontreras@email.tld', profile: { userId: 1, firstName: 'Danielle', lastName: 'Contreras' } },
	//   { id: 2, email: 'afeher@email.tld', profile: { userId: 2, firstName: 'Alfredas', lastName: 'Fehér' } },
	//   { id: 3, email: 'odj@email.tld', profile: { userId: 3, firstName: 'Orpheus', lastName: 'De Jong' } },
	// ]

_Example 3_ produces the same result as _Example 2_ but will be much faster with long arrays.

### 2. Copying an object with filtered properties:

Sometimes you need a copy of an object that only includes certain properties from the original object, or that omits certain properties. This is a great use case for `Array.reduce`.

#### Example 4:

	// Copy an object, retaining allowed properties:

	const person = {
	  firstName: 'Orpheus',
	  lastName: 'De Jong',
	  phone: '+1 123-456-7890',
	  email: 'fake@email.tld',
	};

	const allowedProperties = ['firstName', 'lastName'];

	const allKeys = Object.keys(person);
	const result = allKeys.reduce((next, key) => {
	  if (allowedProperties.includes(key)) {
	  	return { ...next, [key]: person[key] };
	  } else {
	  	return next;
	  }
	}, {});

	// result:
	// { firstName: 'Orpheus', lastName: 'De Jong' }

_Example 4_ reduces the keys from the `person` object into a new object that only contains the properties whose keys are included in the `allowedProperties` array. If you add a new property to the `person` object it will not appear in the result unless you also add the property name to the allowed properties.

#### Example 5:

	// Copy an object, excluding disallowed properties:

	const person = {
	  firstName: 'Orpheus',
	  lastName: 'De Jong',
	  phone: '+1 123-456-7890',
	  email: 'odj@email.tld',
	};

	const disallowedProperties = ['phone', 'email'];

	const allKeys = Object.keys(person);
	const result = allKeys.reduce((next, key) => {
	  if (!disallowedProperties.includes(key)) {
	  	return { ...next, [key]: person[key] };
	  } else {
	  	return next;
	  }
	}, {});

	// result:
	// { firstName: 'Orpheus', lastName: 'De Jong' }

_Example 5_ reduces the keys from the `person` object into a new object that only contains the properties whose keys are _not_ included in the `disallowedProperties` array. If you add a new property to the `person` object it _will_ appear in the result unless you also add the property name to the disallowed properties. If you want to ensure that only certain properties will be included in the result, _Example 4_ is a better choice, but _Example 5_ is useful when you only need to ensure that certain properties will never included.

We can also create generic functions for the reduce statements in examples 4 and 5:

#### Example 6:

	const filterAllowedObjectProperties = (obj, allowedProperties = []) => {
	  return Object.keys(obj).reduce((next, key) => {
		if (allowedProperties.includes(key)) {
		  return { ...next, [key]: obj[key] };
		} else {
		  return next;
		}
	  }, {});
	}

	const filterDisallowedObjectProperties = (obj, disallowedProperties = []) => {
	  return Object.keys(obj).reduce((next, key) => {
		if (!disallowedProperties.includes(key)) {
		  return { ...next, [key]: obj[key] };
		} else {
		  return next;
		}
	  }, {});
	}

### Merging two objects, preferring values from one:

Another common task is to merge an object with another object that contains fallback, or default values for some properties. Sometimes you can do this simply by using object spread, but this can have unexpected consequences when you have null or empty properties:

#### Example 7:

	const obj1 = {
	  key1: 'value 1.1',
	  key2: null,
	  key3: 'value 1.3',
	  key4: ''
	};

	const obj2 = {
	  key1: 'value 2.1',
	  key2: 'value 2.2',
	  key3: 'value 2.3',
	  key4: 'value 2.4',
	  key5: 'value 2.5'
	};

	const result = { ...obj2, ...obj1 };

	// result:
	//  {
	//    key1: 'value 2.1',
	//    key2: null,
	//    key3: 'value 2.3',
	//    key4: '',
	//    key5: 'value 2.5'
	//  };

_Example 7_ creates a new object containing the properties from `obj2` overridden by the properties from `obj1`. Therefore, the values from `obj2` serve as fallback or defaults for the values from `obj1`. Notice that the result retains the `null` and empty string values from `obj1`. That happens because `null` and an empty string are both _defined_ values. We probably didn't want this result but `Array.reduce` offers a solution.

#### Example 8:

	const obj1 = {
	  key1: 'value 1.1',
	  key2: null,
	  key3: 'value 1.3',
	  key4: ''
	};

	const obj2 = {
	  key1: 'value 2.1',
	  key2: 'value 2.2',
	  key3: 'value 2.3',
	  key4: 'value 2.4',
	  key5: 'value 2.5'
	};

	// Spread the keys from both objects into an array.
	const allKeys = [ ...Object.keys(obj1), ...Object.keys(obj2) ];

	// Convert the array of keys to a set to remove duplicate values,
	// then spread the unique values into a new array.
	const uniqueKeys = [ ...new Set(allKeys) ];

	// Reduce the unique keys into a new object containing the value
	// for each key from obj1, falling back to the value from obj2 if
	// obj1[key] is falsey.
	const result = uniqueKeys.reduce((next, key) => {
	  const value = obj1[key] || obj2[key];
	  return { ...next, [key]: value };
	}, {});

	// result:
	// {
	//	 key1: 'value 1.1',
	//   key2: 'value 2.2',
	//   key3: 'value 1.3',
	//   key4: 'value 2.4',
	//   key5: 'value 2.5',
	// }

Note that _Example 8_ uses a naive strategy to decide when to use the fallback value. The fallback value (`obj2[key]`) is used if the preferred value (`obj1[key]`) is _falsey_. That means `undefined`, `null`, an empty string, `0` or `false`. This may not be appropriate for all cases, since any of these values might be acceptable in your application. Revise the fallback condition as necessary. For example, replacing `const value = obj1[key] || obj2[key];` with `const value = (obj1[key] !== undefined && obj1[key] !== null) ? obj1[key] : obj2[key];` will ensure that the fallback value is only used when the preferred value is `undefined` or `null`.

### Parsing search/query strings:

Finally, let's look at another very common task that developers often use a third party library to accomplish: parsing search strings (sometimes called query strings). Modern web browsers provide [URLSearchParams](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams/URLSearchParams) to make quick work of this, but maybe you aren't writing code for a browser, or you have to support Internet Explorer, or you just want to try doing it yourself because that's how we learn. Whatever your reason, `Array.reduce` is your friend.

First, we need a search string. You might get this directly from `window.location.search` in a browser or by parsing an URL. If you use React and react-router you might use the `useLocation` hook:

	`const { search = '' } = useLocation();`

However you get the search string, you'll need to prepare it first:

#### Example 9a:

	// Get a search string:
	const search = '?key1=value%201&key2=value%202&key3=value%203';

	// Remove the leading '?':
	const query = search.replace(/^\?/, '');

	// Split the string on the ampersand to create an array of key-value strings:
	const pairs = query.split('&');

	// pairs:
	// [ 'key1=value%201', 'key2=value%202', 'key3=value%203' ];

Next, reduce the key-value strings into an object by splitting them on the equals sign. The string before `=` is the key and the remainder is the value. The value needs to be decoded with `decodeURIComponent`.

#### Example 9b:

	const params = pairs.reduce((next, pair) => {
	  const [ key, value ] = pair.split('=');
	  const decodedValue = decodeURIComponent(value);
	  return { ...next, [key]: decodedValue };
	}, {});

	// params:
	// {
	//	 key1: 'value 1',
	//   key2: 'value 2',
	//   key3: 'value 3',
	// }

The parser in _Example 9a/9b_ will do the job in many cases, but it's incomplete. Search strings can contain multiple values for each key, and this parser will only retain the last value for each key. Let's fix that.

#### Example 10:

	const search = '?key1=value%201&key2=value%202&key3=value%203.1&key3=value%203.2&key3=value%203.3';
	const query = search.replace(/^\?/, '');
	const pairs = query.split('&');

	const params = pairs.reduce((next, pair) => {
	  const [ key, value ] = pair.split('=');
	  const decodedValue = decodeURIComponent(value);
	  const previousValue = next[key];
	  let nextValue;

	  if (previousValue !== undefined) {
	  	if (Array.isArray(previousValue)) {
	  	  nextValue = [ ...previousValue, decodedValue ];
	  	} else {
	  	  nextValue = [ previousValue, decodedValue ];
	  	}
	  } else {
	  	nextValue = decodedValue;
	  }

	  return { ...next, [key]: nextValue };
	}, {});

	// params:
	// {
	//	 key1: 'value 1',
	//   key2: 'value 2',
	//   key3: [ 'value 3.1', 'value 3.2', 'value 3.3' ],
	// }

_Example 10_ prepares the search string exactly the same way as _Example 9a_. The difference is how the reduce callback handles the value for each key. Here's a step by step breakdown of the callback function:

1. The key-value string (`pair`) is split on `=` to get separate strings for the key and value.
2. The value is decoded with `decodeURIComponent`.
3. The accumulator (`next`) is checked to determine if there is a previous value for the key.
4. If there is a previous value (`previousValue !== undefined`) it is checked to determine whether it's an array.
5. If the previous value is an array, the decoded value is appended to it. (`nextValue = [ ...previousValue, decodedValue ];`) If the previous value isn't an array, a new array is created containing the previous and decoded values. (`nextValue = [ previousValue, decodedValue ];`)
6. If there is no previous value, the next value is set to the decoded value. (`nextValue = decodedValue;`)

The resulting `params` object contains string values for `key1` and `key2`, and an array containing the three strings for `key3` in the order in which they appeared in the search string.

Like we did in _Example 1_, we can clarify the process by examining the state of each iteration:

1. **Accumulator (`next`):** `{}` (the initial value); **Value (`pair`):** `'key1=value%201`; **Returns:** `{ key1: 'value 1' }`;
2. **Accumulator:** `{ key1: 'value 1' }`; **Value:** `'key2=value%202`; **Returns:** `{ key1: 'value 1', key2: 'value 2' }`;
3. **Accumulator:** `{ key1: 'value 1', key2: 'value 2' }`; **Value:** `'key3=value%203.1`; **Returns:** `{ key1: 'value 1', key2: 'value 2', key3: 'value 3.1' }`;
4. **Accumulator:** `{ key1: 'value 1', key2: 'value 2', key3: 'value 3.1' }`; **Value:** `'key3=value%203.2`; **Returns:** `{ key1: 'value 1', key2: 'value 2', key3: ['value 3.1', 'value 3.2'] }`;
5. **Accumulator:** `{ key1: 'value 1', key2: 'value 2', key3: ['value 3.1', 'value 3.2'] }`; **Value:** `'key3=value%203.3`; **Returns:** `{ key1: 'value 1', key2: 'value 2', key3: ['value 3.1', 'value 3.2', 'value 3.3'] }`;

## Summary:

`Array.reduce` is sort of a Swiss army knife that you can use to solve a wide variety of problems. I encourage you to explore it and try applying it in situations you might not have considered.