Requirement: We have an array of objects that is deeply nested. We want to bring all the nested objects into the array at 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 of the objects have children, it pushes them in children
array and pass them again to getMembers
method. This goes on recursively until no new child is found. The results are concatenated at each step and final result is returned.
We can shorten the code by remove 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 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 original object has children 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 that has no children, else returns new array with member and the result of recursively called _.flatMapDeep
.
To delete children in the flatten array, while keeping the original intact, we need to return a copy of member using spread operator in which children is 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 [member, _.flatMapDeep(mem.children, getMembers)]; // return copied, but pass original to flatMapDeep
}
_.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 objects that is not frequently updated, we can keep its flat copy, and update it in on every add, update or delete made on the original.
Now to lookup 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
- JavaScript: Change The Behavior of A Class Method At Runtime
- JavaScript Rollbar Unknown Unhandled Rejection Error Getting Reason From Event
- JavaScript Disable console.log On Production Environment
- How To Publish And Use A Private JavaScript Library Without NPM Registry?
- JavaScript Recursion With Default Function Parameter
- What Is Destructuring And Restructuring Design Pattern In JavaScript?
- JavaScript: Difference Between Module, Library, Package, API, Framework And Application