Home > Software design >  Add object to array recursively in JavaScript
Add object to array recursively in JavaScript

Time:06-08

I'm working with data via the Contentful API and I'm trying to build up a structure of the website I'm building. I'm using the GraphQL API and getting Contentful pages, but, due to rate-limiting, I can't go super deep with my query when it comes to getting references and hierarchical structure.

This has left me querying my "pages" content type and getting each page and getting its parent page ID, just one level up. The theory being that if I do this for every page, I can build up the page structure recursively by running some JS during the build (I'm using Eleventy to build the site).

I need a function to generate me a breadcrumbs array, ideally I'd give the function a single page's ID and it would track through the data getting me the details of the parent, the parent's parent and so on until there are no more parents.

I've got the following code, split into two functions at the moment (although not sure if that's the best approach!)

  /**
   * This takes an ID and finds it within the contentfulPages array (the result
   * of the GraphQL query to Contentful)
   * It returns an object containing the page's ID, slug (for the URL), title
   * (for the readable text) and the parentPageId so we can use that to get the
   * same details for the parent page
   */
  function getParentPage(pageId) {
    const { id, slug, title, parentPageId } = contentfulPages.find(
      (page) => page.id === pageId
    );
    return { id, slug, title, parentPageId };
  }

  /**
   * This takes an ID and passes it to getParentPage to get the first parentPage
   * details. If there is another parent page, it calls itself with the newly 
   * found parent page and pushes that object to the breadcrumbArray. Because it
   * returns the breadcrumb array, each iteration is a more deeply nested array 
   * as opposed to an object at the same level
   */
  function getBreadcrumb(pageId) {
    let breadcrumbArray = [];
    const { id, slug, title, parentPageId } = getParentPage(pageId);
    breadcrumbArray.push({ id, slug, title, parentPageId });
    if (parentPageId) {
      breadcrumbArray.push(getBreadcrumb(parentPageId));
    }
    return breadcrumbArray;
  }

I can see why it's doing what it's doing, but I don't know how to fix it - I want the end result to be a flat array of objects, but the recursive function is causing me problems I think, because I think it has to return something, but I'm not sure how to do that!

An example for a three-levels deep page is this:

// Expected:
[
  {
    id: 'page-3',
    slug: 'courses',
    title: 'Courses',
    parentPageId: 'page-2'
  },
  {
    id: 'page-2',
    slug: 'training',
    title: 'Training',
    parentPageId: 'page-1'
  },
  {
    id: 'page-1',
    slug: 'services',
    title: 'Services',
    parentPageId: false
  },
]


// Actual:
[
  {
    id: 'page-3',
    slug: 'courses',
    title: 'Courses',
    parentPageId: 'page-2'
  },
  [
    {
      id: 'page-2',
      slug: 'training',
      title: 'Training',
      parentPageId: 'page-1'
    },
    [ 
      {
        id: 'page-1',
        slug: 'services',
        title: 'Services',
        parentPageId: false
      },
    ]
  ]
]

Assistance would be much appreciated!

CodePudding user response:

Rather than pushing the entire returned array from getBreadcrumb into your result, you can instead push the individual array elements from the returned array using the spread syntax (...):

breadcrumbArray.push(...getBreadcrumb(parentPageId))

Note that the above will fail if the array returned by getBreadcrumb() is very large an exceeds the max-argument count that you can pass to the .push() function. You do always have the option of doing what you're currently doing, and then calling .flat(Infinity) on your returned result. This does add an additional pass over your array though which can be avoided. You can overcome these issues by instead returning a new array, with your returned array spread into that:

function getBreadcrumb(pageId) {
  const { id, slug, title, parentPageId } = getParentPage(pageId);
  return [{ id, slug, title, parentPageId }, ...(parentPageId ? getBreadcrumb(parentPageId) : [])];
}

The above will create a new array, with your {id, slug, ...} object as the first element, followed by either nothing (if parentPageId is falsy), or the array elements from that array returned by the call to getBreadcrumb(parentPageId))

CodePudding user response:

Here's a non-recursive approach, use a while loop, and re-call getParentPage():

function getBreadcrumb(pageId) {
  let breadcrumbArray = [];
  let { id, slug, title, parentPageId } = getParentPage(pageId);
  breadcrumbArray.push({ id, slug, title, parentPageId });
  
  // while parentPageId === false, execute getParentPage with found id
  while ( parentPageId ) {
      pageObj = getParentPage(id);
      breadcrumbArray.push(pageObj);
      // update found id and parentPageId for while loop
      id = pageObj.parentPageId;
      parentPageId = pageObj.parentPageId;
  }
  return breadcrumbArray;
}

// okay to ignore the rest...
// parent page mock up, randomly return a parentPageId that equates to false
function getParentPage(pageId) {
  return { id: 1, slug: "slug", title: "title", parentPageId: Math.floor(Math.random() * 2) };
}
console.log(getBreadcrumb(3));

  • Related