JavaScript Flatten Deeply Nested Array of Objects Into Single Level Array

Using plain JavaScript, and lodash's flatMapDeep method.

Flatten deeply nested array

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

When you purchase through links on techighness.com, I may earn an affiliate commission.