Requirement: We have a deeply nested array of objects. We want to bring all the nested objects into the array at the root level.
Following is the example array familyTree
that has multiple people in the root, and many of them have children array containing further members:
const familyTree = [
{
id: "23b9dbff",
name: "Jessie",
age: 50,
children: [
{
id: "5c0f3094",
name: "Peter",
age: 20
},
{
id: "c1484221",
name: "Paul",
age: 32,
children: [
{
id: "2e6d866e",
name: "Carol",
age: 12
},
{
id: "e48a27ad",
name: "Hester",
age: 15
}
]
},
{
id: "8a265c23",
name: "Hilda",
age: 25
}
]
},
{
id: "53164b2b",
name: "Mathew",
age: 70,
children: [
{
id: "b14a960c",
name: "Spencer",
age: 45,
children: [
{
id: "ff3c260c",
name: "Joseph",
age: 22
},
{
id: "7c60920a",
name: "Robert",
age: 27,
children: [
{
id: "0e11874f",
name: "Ian",
age: 2
}
]
}
]
}
]
},
{
id: "5a4bdc98",
name: "Claire",
age: 63,
children: [
{
id: "014b62a3",
name: "Adrian",
age: 41
},
{
id: "a1899541",
name: "Julie",
age: 32,
children: [
{
id: "013362a3",
name: "Patricia",
age: 4
}
]
}
]
}
];
We can flatten the array in one of the following ways:
1. Using Plain JavaScript (es6)
const familyTree = [
// as above
];
const getMembers = (members) => {
let children = [];
const flattenMembers = members.map(m => {
if (m.children && m.children.length) {
children = [...children, ...m.children];
}
return m;
});
return flattenMembers.concat(children.length ? getMembers(children) : children);
};
getMembers(familyTree);
Here, we recursively call getMembers
method, which maps through the array at each level and returns the objects. If any objects have children, it pushes them into the children
array and passes them to the getMembers
method. This goes on recursively until no new child is found. The results are concatenated at each step, and the final result is returned.
We can shorten the code by removing flattenMember
variable:
const getMembers = (members) => {
let children = [];
return members.map(m => {
if (m.children && m.children.length) {
children = [...children, ...m.children];
}
return m;
}).concat(children.length ? getMembers(children) : children);
};
The result has all the members present in the root array as below:
[
{
id: '23b9dbff',
name: 'Jessie',
age: 50,
children: [ [Object], [Object], [Object] ]
},
{ id: '53164b2b', name: 'Mathew', age: 70, children: [ [Object] ] },
{
id: '5a4bdc98',
name: 'Claire',
age: 63,
children: [ [Object], [Object] ]
},
{ id: '5c0f3094', name: 'Peter', age: 20 },
{
id: 'c1484221',
name: 'Paul',
age: 32,
children: [ [Object], [Object] ]
},
{ id: '8a265c23', name: 'Hilda', age: 25 },
{
id: 'b14a960c',
name: 'Spencer',
age: 45,
children: [ [Object], [Object] ]
},
{ id: '014b62a3', name: 'Adrian', age: 41 },
{ id: 'a1899541', name: 'Julie', age: 32, children: [ [Object] ] },
{ id: '2e6d866e', name: 'Carol', age: 12 },
{ id: 'e48a27ad', name: 'Hester', age: 15 },
{ id: 'ff3c260c', name: 'Joseph', age: 22 },
{ id: '7c60920a', name: 'Robert', age: 27, children: [ [Object] ] },
{ id: '013362a3', name: 'Patricia', age: 4 },
{ id: '0e11874f', name: 'Ian', age: 2 }
]
Remove Children
Since we’re flattening the array, we can delete the children node, as we might not be interested in keeping it. But since members are accessed by reference, if we delete children reference from a member, it will also be removed from the original familyTree
. So we use the spread operator.
const getMembers = (members) => {
let children = [];
return members.map(mem => {
const m = {...mem}; // use spread operator
if (m.children && m.children.length) {
children = [...children, ...m.children];
}
delete m.children; // this will not affect the original array object
return m;
}).concat(children.length ? getMembers(children) : children);
};
We can verify that the original object has children’s value intact:
console.log("Flat Array Object: \n", getMembers(familyTree)[0], "\n")
console.log("Original Array Object: \n", familyTree[0])
Result:
Flat Array Object:
{ id: '23b9dbff', name: 'Jessie', age: 50 } // children reference removed
Original Array Object:
{
id: '23b9dbff',
name: 'Jessie',
age: 50,
children: [
{ id: '5c0f3094', name: 'Peter', age: 20 },
{ id: 'c1484221', name: 'Paul', age: 32, children: [Array] }, // still has children
{ id: '8a265c23', name: 'Hilda', age: 25 }
]
}
2. Using Lodash flatMapDeep Method
The above same thing can be done more precisely using lodash’s flatMapDeep method as follows:
const _ = require("lodash");
const familyTree = [
// ...
];
const getMembers = (member)=>{
if(!member.children || !member.children.length){
return member;
}
return [member, _.flatMapDeep(member.children, getMembers)];
}
_.flatMapDeep(familyTree, getMembers);
We pass getMembers
as second argument of _.flatMapDeep
. This method returns the member with no children, else returns a new array with the member, and the result of recursively called _.flatMapDeep
.
To delete children in the flattened array while keeping the original intact, we need to return a copy of the member using the spread operator in which children are removed. But we still pass the original member to _.flatMapDeep
because it needs to process children.
const getMembers = (mem) => {
const member = { ...mem }; // copy
delete member.children;
if (!mem.children || !mem.children.length) {
return member; // return copied
}
// return copied, but pass original to flatMapDeep
return [
member,
_.flatMapDeep(mem.children, getMembers)
];
}
_.flatMapDeep(familyTree, getMembers)
Advantages
One of the benefits of flattening an array is quick look-up time, especially in front-end applications like React, Angular, or Vue, where the state might become deeply nested and complex. For a nested array of rarely updated objects, we can keep its flat copy, and update it on every add, update or delete made on the original.
Now to look up anything, we just need to traverse the flat array in time O(n) instead of going deep in the original tree on each lookup, which is expensive.
For example. To find out that if members with age less than or equal to 2 exists in familyTree
, we can check the flat array using a method like _.some
from lodash. We can also _.find
to get the member object:
const _ = require("lodash");
const familyTree = [
// ...
];
const flatArray = _.flatMapDeep(familyTree, getMembers);
// ...
console.log(_.some(flatArray, ({ age }) => age <= 2));
// => true
console.log(_.find(flatArray, ({ age }) => age <= 2));
// => { id: '0e11874f', name: 'Ian', age: 2 }
See also
- SignatureDoesNotMatch: The request signature we calculated does not match the signature you provided. Check your key and signing method.
- Yup Date Format Validation With Moment JS
- Yup Number Validation: Allow Empty String
- Exactly Same Query Behaving Differently in Mongo Client and Mongoose
- JavaScript Unit Testing JSON Schema Validation
- Reduce JS Size With Constant Strings
- JavaScript SDK