Recursive function used to update an array of objects by dynamically mapping replies to comments

I have received a collection of comments from a graphql backend structured like this:

[
        {
            "__typename": "Comment",
            "id": "1",
            "userId": "1",
            "postId": "1",
            "parentCommentId": null,
            "content": "test 1"
        },
        {
            "__typename": "Comment",
            "id": "2",
            "userId": "1",
            "postId": "1",
            "parentCommentId": null,
            "content": "this is a comment"
        },
        {
            "__typename": "Comment",
            "id": "34",
            "userId": "1",
            "postId": "1",
            "parentCommentId": "1",
            "content": "reply to test1"
        },
        {
            "__typename": "Comment",
            "id": "35",
            "userId": "1",
            "postId": "1",
            "parentCommentId": "34",
            "content": "nested reply to \"reply to test1\"\n\n"
        },
        {
            "__typename": "Comment",
            "id": "36",
            "userId": "1",
            "postId": "1",
            "parentCommentId": "34",
            "content": "test?"
        }
    ]

Comments with parentCommentId === null are the main level comments, while those with parentCommentId !== null are replies to the comments where id === parentCommentId

I aim to convert this data structure into something similar to this:

[{
    "__typename": "Comment",
    "id": "1",
    "userId": "1",
    "postId": "1",
    "parentCommentId": null,
    "content": "test1",
    "replies": [{
      "__typename": "Comment",
      "id": "34",
      "userId": "1",
      "postId": "1",
      "parentCommentId": "1",
      "content": "reply to test1",
      "replies": [{
        "__typename": "Comment",
        "id": "35",
        "userId": "1",
        "postId": "1",
        "parentCommentId": "34",
        "content": "reply to test1"
      }]
    }]
  },
  {
    "__typename": "Comment",
    "id": "2",
    "userId": "1",
    "postId": "1",
    "parentCommentId": null,
    "content": "this is a comment",
    "replies": []
  }
]

I have created a function for the data transformation:

function formatData(comments: Array < IComment > ) {
  let commentList = Array < IComment > ();

  // Add top-level comments without `parentCommentId`.
  for (let i = 0; i < comments.length; i++) {
    if (!comments[i].parentCommentId) {
      commentList.push({ ...comments[i],
        replies: []
      });
    }
  }

  for (let i = 0; i < comments.length; i++) {
    if (comments[i].parentCommentId) {
      const reply = comments[i];
      mapReplyToComment(commentList, reply);
    }
  }


  return commentList;

  function mapReplyToComment(
    commentList: Array < IComment > ,
    reply: IComment
  ): any {
    return commentList.map((comment) => {
      if (!comment.replies) {
        comment = { ...comment,
          replies: []
        };
      }
      if (comment.id === reply.parentCommentId) {
        comment.replies.push(reply);

        return comment;
      } else {
        return mapReplyToComment(comment.replies, reply);
      }
    });
  }
}

However, this only works for one level deep into the object tree. The replies to replies are not included in the object.

This is my current output:

[{
    "__typename": "Comment",
    "id": "1",
    "userId": "1",
    "postId": "1",
    "parentCommentId": null,
    "content": "test1",
    "replies": [{
      "__typename": "Comment",
      "id": "34",
      "userId": "1",
      "postId": "1",
      "parentCommentId": "1",
      "content": "reply to test1"
      // -- There should be another node of "replies" here
    }]
  },
  {
    "__typename": "Comment",
    "id": "2",
    "userId": "1",
    "postId": "1",
    "parentCommentId": null,
    "content": "this is a comment",
    "replies": []
  }
]

Could you please help me understand what I am doing wrong and provide some guidance?

Edit:

After taking @Nina Scholz's suggestion, I have come up with this solution:

function formatData(data: Array < IComment >, root: string) {
  const temp: any = {};

  data.forEach((comment: IComment) => {
    const parentCommentId = comment.parentCommentId ? ? root;

    if (temp[parentCommentId] == null) {
      temp[parentCommentId] = {};
    }

    if (temp[parentCommentId].replies == null) {
      temp[parentCommentId].replies = [];
    }

    if (temp[comment.id] == null) {
      temp[parentCommentId].replies.push(
        Object.assign((temp[comment.id] = {}), comment)
      );
    } else {
      temp[parentCommentId].replies.push(
        Object.assign(temp[comment.id], comment)
      );
    }
  });
  return temp[root].replies;
}

Answer №1

You can achieve a single iteration by utilizing an object that tracks the parent-child and child-parent relationships.

const
    getTree = (data, root) => {
        const t = {};
        data.forEach(o =>
            ((t[o.parentCommentId] ??= {}).replies ??= []).push(
                Object.assign(t[o.id] ??= {}, o)
            )
        );
        return t[root].replies;
    },
    data = [{ __typename: "Comment", id: "1", userId: "1", postId: "1", parentCommentId: null, content: "test 1" }, { __typename: "Comment", id: "2", userId: "1", postId: "1", parentCommentId: null, content: "this is a comment" }, { __typename: "Comment", id: "34", userId: "1", postId: "1", parentCommentId: "1", content: "reply to test1" }, { __typename: "Comment", id: "35", userId: "1", postId: "1", parentCommentId: "34", content: "nested reply to \"reply to test1\"\n\n" }, { __typename: "Comment", id: "36", userId: "1", postId: "1", parentCommentId: "34", content: "test?" }],
    tree = getTree(data, null);

console.log(tree);
.as-console-wrapper { max-height: 100% !important; top: 0; }

Answer №2

Having trouble understanding the previous answer, I decided to share my own approach in breaking down the steps into separate functions: (Keep in mind that I included the replies array in your provided sample data)

let commentsData = [{    "__typename": "Comment",    "id": "1",    "userId": "1",    "postId": "1",    "parentCommentId": null,    "content": "test 1",    "replies": []  },
  {    "__typename": "Comment",    "id": "2",    "userId": "1",    "postId": "1",    "parentCommentId": null,    "content": "this is a comment",    "replies": []  },
  {    "__typename": "Comment",    "id": "34",    "userId": "1",    "postId": "1",    "parentCommentId": "1",    "content": "reply to test1",    "replies": []  },
  {    "__typename": "Comment",    "id": "35",    "userId": "1",    "postId": "1",    "parentCommentId": "34",    "content": "nested reply to \"reply to test1\"\n\n",    "replies": []  },
  {    "__typename": "Comment",    "id": "36",    "userId": "1",    "postId": "1",    "parentCommentId": "34",    "content": "test?",    "replies": []  }
]

function findLowestComment(dataArray) {
  for (let i = 0; i < dataArray.length; i++) {
    let comment = dataArray[i]
    isLowest = true
    if (comment.parentCommentId == null) {
      continue
    }
    for (let j = 0; j < dataArray.length; j++) {
      if (dataArray[j].id != comment.id &&
        dataArray[j].parentCommentId == comment.id &&
        dataArray[j].parentCommentId != null) {
        isLowest = false;
        break
      }
    }
    if (isLowest) {
      return i
    }
  }
}

function insertIntoParent(dataArray, commentIndex) {
  for (let j = 0; j < dataArray.length; j++) {
    if (dataArray[j].id == dataArray[commentIndex].parentCommentId) {
      dataArray[j].replies.push(dataArray[commentIndex])
      dataArray.splice(commentIndex, 1)
      break
    }
  }
}

function mapComments(dataArray) {
  for (let j = 0; j < dataArray.length; j++) {
    let lowestIndex = findLowestComment(dataArray)
    insertIntoParent(dataArray, lowestIndex)
  }
}

mapComments(commentsData)
console.log(JSON.stringify(commentsData, undefined, 2))

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

The significance of 'this' in an Angular controller

Forgive me for what may seem like a silly question, but I believe it will help clarify my understanding. Let's dive into JavaScript: var firstName = "Peter", lastName = "Ally"; function showFullName () { // The "this" inside this func ...

Tips for ensuring the CSRF token functions properly on the browser when utilizing Django and React

Apologies in advance if this question seems beginner-friendly, but I have developed an application with Django backend and React frontend. I am currently working on implementing the CSRF token for the post request on the create endpoint using the code snip ...

Vue Framework 7 incorporates a validation feature that ensures successful outcomes

In my current project using Framework7 Vue with version 4.4.3, I am facing a challenge in validating a form upon submission. I came across this helpful code snippet: $$('.save').on('click', function(e){ e.preventDefault(); if ...

Delay in loading Jquery accordion due to value binding

I've noticed that my jquery accordion takes a significant amount of time to collapse when the page initially loads. After some investigation, I realized that the issue lies in the fact that there are numerous multiselect listboxes within the accordio ...

Dynamic item addition feature activated by button on contact form

I am looking to create a form that includes the standard name, phone, email fields as well as a dropdown for selecting products and a text box for inputting quantity. The unique aspect is allowing users to add multiple products (dropdown and quantity textb ...

Javascript keycode used to target the element when a key is pressed

In my current project, I am utilizing a code snippet to attach a KeyDown event handler to any element within the HTML form. for(var i=0;i<ele.length;i++) { ele[i].onkeydown = function() { alert('onkeydown'); } } I am ...

loop through nested arrays

My goal is to use ng repeat in Angular to iterate through a child array of a multidimensional array. The json object I am working with is as follows: $scope.items = [{ "id":1, "BasisA":"1", "Basis":true, "personSex": ...

JavaScript guide: Deleting query string arrays from a URL

Currently facing an issue when trying to remove query string arrays from the URL. The URL in question looks like this - In Chrome, it appears as follows - Var url = "http://mywebsite.com/innovation?agenda%5B%5D=4995&agenda%5B%5D=4993#ideaResult"; ...

Having trouble sending values via POST request in Node.js using Express

Currently, I am in the process of learning how to use Express (v4) with Node.js. My main goal right now is to create a basic REST API. This API specifically focuses on one endpoint: /orders. The main functionality I am trying to achieve is the ability to r ...

Implementing a Bootstrap bottom popover when an image is clicked using JavaScript

Does anyone know how to display a Bootstrap bottom pophover when an image button is clicked using JavaScript? Appreciate any help! ...

My goal is to programmatically eliminate captcha from a website

Is there a way to automatically remove capture from a Page using javascript (Greasemonkey)? The page's HTML structure seems complex, so any suggestions on how to achieve this would be greatly appreciated. <div class="wrapper"> <d ...

Learn how to change the input source in Ratchet's chat app to come from a text box on the webpage instead of the console

I followed the guidelines on to create a chat app. However, I now want to input data from a text box instead of using conn.send() in the console. How can I achieve this using php? I was successful in redirecting the sent message to an html element like ...

Do you want to reset the validation for the paper input?

I am encountering an issue with a paper-input element in my code. Here is what it looks like: <paper-input id="inputForValidation" required label="this input is manually validated" pattern="[a-zA-Z]*" error-message="letters only!"></paper-input&g ...

The post method in Express.js is having difficulty parsing encoded data accurately

I'm currently working on an AngularJS code that sends a POST request like this: var req = { method: 'POST', url: 'http://localhost:3300/addInventoryItem', headers: { 'Content-Type': 'application/x-www-form- ...

How can I use jQuery to add styling to my menu options?

Looking to customize my menu items: <ul> <li class="static"> <a class="static menu-item" href="/mySites/AboutUs">About Us</a> </li> <li class="static"> <a class="static-menu-item" href="/m ...

Display error messages upon submitting the form

I am currently working on an Angular 6 Form with validation. My main goal is to display error messages only after the form has been submitted. It is crucial that these messages remain constant while the user types in the input field. For instance, if a use ...

What are the best methods for embedding HTML code from one page into another within an HTML document?

My current challenge involves working on a webpage that contains an iframe with a specific source. I am now tasked with creating an offline version of the same page, without having multiple pages. Essentially, I need to find a way to store and access the ...

Exploring the integration of d3 in an Express application - encountering an error: document is not recognized

I am facing a challenge in my expressjs application where I need to dynamically render vertices in a graph using d3. However, the code execution order seems to be causing issues for me. When attempting to use the d3.select function, I encounter the followi ...

communication between a Windows C# application and a web-based JavaScript interface

Similar Question: how to get variable in desktop c# program from web javascript application Encountered an issue involving two applications: a desktop application written in C# and a web application in JavaScript. The need has arisen to transfer certa ...

Pause the initial ajax data loading in Tabulator.js and instead load data only after the first filter is applied

Currently, I am utilizing the tabulator.js library and hoping to delay the loading of data until after the first filter is applied, rather than immediately upon page load. Is there a way to achieve this functionality using something like: initialAjaxLoa ...