JavaScript Flatten Deeply Nested Array of Objects Into Single Level Array

Using plain JavaScript, and lodash's flatMapDeep method

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