я обычно как-то так - разбиваю исходный массив на рутовые элементы и хэш-дочерних и в рекурсию
var flat = [
{id: 1, parentId: null},
{id: 2, parentId: null},
{id: 3, parentId: 1},
{id: 4, parentId: 1},
{id: 5, parentId: null},
{id: 6, parentId: 3},
{id: 7, parentId: 5}
];
function trace(el) {
if (items.childsByPid[
el.id]) {
el.childList = items.childsByPid[
el.id];
trace(items.childsByPid[
el.id]);
}
return el;
}
var items = flat.reduce((acc, item) => {
if (!item.parentId) {
acc.roots.push({ ...item, childList: []})
} else {
acc.childsByPid[item.parentId] = acc.childsByPid[item.parentId] || [];
acc.childsByPid[item.parentId].push({ ...item, childList: []})
}
return acc;
}, { roots: [], childsByPid: {} });
items.roots.reduce((acc, item) => {
return (acc.push(trace(item)), acc)
}, []);