30
loading...
This website collects cookies to deliver better user experience
Array.reduce
method to transform objects.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 — 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:const numbers = [1, 2, 3, 4, 5];
const sum = numbers.reduce((next, number) => {
return next + number;
}, 0);
Array.reduce
accepts two arguments: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.number
in the example).0
.Example 1
will execute the callback function five times with the following values:next
): 0
(the initial value); Value (number
): 1
; Returns: 1
;1
; Value: 2
; Returns: 3
;3
; Value: 3
; Returns: 6
;6
; Value: 4
; Returns: 10
;10
; Value: 5
; Returns: 15
;sum
will be 15
.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.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.const users = [
{ id: 1, email: '[email protected]' },
{ id: 2, email: '[email protected]' },
{ id: 3, email: '[email protected]' },
];
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: '[email protected]', profile: { userId: 1, firstName: 'Danielle', lastName: 'Contreras' } },
// { id: 2, email: '[email protected]', profile: { userId: 2, firstName: 'Alfredas', lastName: 'Fehér' } },
// { id: 3, email: '[email protected]', profile: { userId: 3, firstName: 'Orpheus', lastName: 'De Jong' } },
// ]
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:const users = [
{ id: 1, email: '[email protected]' },
{ id: 2, email: '[email protected]' },
{ id: 3, email: '[email protected]' },
];
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: '[email protected]', profile: { userId: 1, firstName: 'Danielle', lastName: 'Contreras' } },
// { id: 2, email: '[email protected]', profile: { userId: 2, firstName: 'Alfredas', lastName: 'Fehér' } },
// { id: 3, email: '[email protected]', profile: { userId: 3, firstName: 'Orpheus', lastName: 'De Jong' } },
// ]
Array.reduce
.// Copy an object, retaining allowed properties:
const person = {
firstName: 'Orpheus',
lastName: 'De Jong',
phone: '+1 123-456-7890',
email: '[email protected]',
};
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' }
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.// Copy an object, excluding disallowed properties:
const person = {
firstName: 'Orpheus',
lastName: 'De Jong',
phone: '+1 123-456-7890',
email: '[email protected]',
};
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' }
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.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;
}
}, {});
}
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'
// };
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.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',
// }
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
.Array.reduce
is your friend.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();`
// 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' ];
=
is the key and the remainder is the value. The value needs to be decoded with decodeURIComponent
.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',
// }
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' ],
// }
pair
) is split on =
to get separate strings for the key and value.decodeURIComponent
.next
) is checked to determine if there is a previous value for the key.previousValue !== undefined
) it is checked to determine whether it's an array.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 ];
)nextValue = decodedValue;
)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.next
): {}
(the initial value); Value (pair
): 'key1=value%201
; Returns: { key1: 'value 1' }
;{ key1: 'value 1' }
; Value: 'key2=value%202
; Returns: { key1: 'value 1', key2: 'value 2' }
;{ key1: 'value 1', key2: 'value 2' }
; Value: 'key3=value%203.1
; Returns: { key1: 'value 1', key2: 'value 2', key3: 'value 3.1' }
;{ 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'] }
;{ 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'] }
;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.