Search code examples
arraysjsonreactjsjavascript-objects

Massage JSON response to fit into treeData structure for react-simple-tree-menu


I have a React component that retrieves an array of objects (key-value pairs) from a REST API via an HTML endpoint:

[
  { 
    "id": 1,
    "grouping1": "first-level-node-1",
    "grouping2": "second-level-node-1",
    "theThing": "third-level-node-1",
    "someData": "data here that is associated with theThing",
    "someUrl": "http://someurl.com"
  },
  {
    "id": 2,
    "grouping1": "first-level-node-2",
    "grouping2": "second-level-node-1",
    "theThing":  "third-level-node-1",
    .
    .
    .
  }
]

I am trying to manipulate the JSON response so that it gets displayed with react-simple-tree-menu. To generate a TreeMenu, data needs to be provided as an array:

// as an array
const treeData = [
  {
    key: 'first-level-node-1',
    label: 'Node 1 at the first level',
    ..., // any other props you need, e.g. url
    nodes: [
      {
        key: 'second-level-node-1',
        label: 'Node 1 at the second level',
        nodes: [
          {
            key: 'third-level-node-1',
            label: 'Last node of the branch',
            nodes: [] // you can remove the nodes property or leave it as an empty array
          },
        ],
      },
    ],
  },
  {
    key: 'first-level-node-2',
    label: 'Node 2 at the first level',
  },
];

or as an object:

// or as an object
const treeData = {
  'first-level-node-1': {               // key
    label: 'Node 1 at the first level',
    index: 0, // decide the rendering order on the same level
    ...,      // any other props you need, e.g. url
    nodes: {
      'second-level-node-1': {
        label: 'Node 1 at the second level',
        index: 0,
        nodes: {
          'third-level-node-1': {
            label: 'Node 1 at the third level',
            index: 0,
            nodes: {} // you can remove the nodes property or leave it as an empty array
          },
        },
      },
    },
  },
  'first-level-node-2': {
    label: 'Node 2 at the first level',
    index: 1,
  },
};

I have tried categorizing the JSON response based on grouping1 (first level node) and grouping2 (second level node) to make it 'fit' into the treeData:

  const fetchItems = async () => {
    const data = await fetch('http://localhost:3001/stuff');
    const input = await data.json();
    const output = input.reduce((acc, item) => ({
      ...acc,
      [item.grouping1]: {
        ...acc[item.grouping1],
        [item.grouping2]: [
          ...(acc[item.gropuing1] && acc[item.grouping1][item.grouping2] || []),
          item,
        ]
      }
    }), {})

and now I have objects (grouping1) that contain objects (grouping2) that contain the array of key-value pairs.

  first-level-node-1:
    second-level-node-1: Array(4)
      0: {id: 1, grouping1: "first-level-node-1", grouping2: "second-level-node-1", theThing: "third-level-node-1"}
      .
      .
      .
  first-level-node-2:
    second-level-node-1: Array(16)
      0: {id: 2, grouping1: "first-level-node-2", grouping2: "second-level-node-1", theThing: "third-level-node-1"}
      .
      .
      .

But this isn't the treeData structure that react-simple-tree-menu wants. How can I massage the JSON response to fit the treeData structure?

Here's an excellent write-up for how to get a Sidebar Menu up and running in React, but nothing that shows how to get a typical JSON response to fit into the required structure.

Update:

The following is the TreeMenu data that controls this react-simple-tree-menu component

<TreeMenu
  data={[
    {
      key: 'mammal',
      label: 'Mammal',
      nodes: [
        {
          key: 'canidae',
          label: 'Canidae',
          nodes: [
            {
              key: 'dog',
              label: 'Dog',
              nodes: [],
              url: 'https://www.google.com/search?q=dog'
            },
            {
              key: 'fox',
              label: 'Fox',
              nodes: [],
              url: 'https://www.google.com/search?q=fox'
            },
            {
              key: 'wolf',
              label: 'Wolf',
              nodes: [],
              url: 'https://www.google.com/search?q=wolf'
            }
          ],
          url: 'https://www.google.com/search?q=canidae'
        }
      ],
      url: 'https://www.google.com/search?q=mammal'
    },
    {
      key: 'reptile',
      label: 'Reptile',
      nodes: [
        {
          key: 'squamata',
          label: 'Squamata',
          nodes: [
            {
              key: 'lizard',
              label: 'Lizard',
              url: 'https://www.google.com/search?q=lizard'
            },
            {
              key: 'snake',
              label: 'Snake',
              url: 'https://www.google.com/search?q=snake'
            },
            {
              key: 'gekko',
              label: 'Gekko',
              url: 'https://www.google.com/search?q=gekko'
            }
          ],
          url: 'https://www.google.com/search?q=squamata'
        }
      ],
      url: 'https://www.google.com/search?q=reptile'
    }
  ]}
  debounceTime={125}
  disableKeyboard={false}
  hasSearch
  onClickItem={function noRefCheck(){}}
  resetOpenNodesOnDataUpdate={false}
/>

If I'm understanding this correctly, Mammal is first-level-node-1 and Reptile is first-level-node-2. Canidae and Squamata are both second-level-node-1 under their respective first-level-nodes. Dog, Fox and Wolf are third-level-node-1, node-2 and node-3 respectively. Lizard, Snake and Gekko are also third-level-node-1, node-2 and node-3. The example I used at the very top of this post may be confusing things. My apologies if that is the case.

Here is the JSON data that more closely resembles what I'm working with:

[
  {
    "id": 2,
    "grouping1": "I124",
    "grouping2": "Cross_Streets",
    "theThing": "12th",
    "url": "http://url2.com"
  },
  {
    "id": 3,
    "grouping1": "I124",
    "grouping2": "Cross_Streets",
    "theThing": "13th",
    "url": "http://url3.com"
  },
  {
    "id": 4,
    "grouping1": "I124",
    "grouping2": "Cross_Streets",
    "theThing": "4th",
    "url": "http://url4.com"
  },
  {
    "id": 14,
    "grouping1": "I124",
    "grouping2": "Ramps",
    "theThing": "Ramp_A",
    "url": "http://url14.com"
  },
  {
    "id": 15,
    "grouping1": "I124",
    "grouping2": "Ramps",
    "theThing": "Ramp_B",
    "url": "http://url15.com"
  },
  {
    "id": 41,
    "grouping1": "I75",
    "grouping2": "Cross_Streets",
    "theThing": "100th",
    "url": "http://url41.com"
  }
]

The goal is to make the above JSON look like this in react-simple-tree-menu:

+ I124
    + Cross_Streets
        12th
        13th
        4th
    + Ramps
        Ramp_A
        Ramp_B
+ I75
    + Cross_Streets
        4th

Solution

  • Here's a possible solution that you could apply, however, I am not sure where you are getting the labels from, but I'll leave that up to you.

    Here's an example with an object as the result:

    const DATA = [
      {
        id: 2,
        grouping1: "I124",
        grouping2: "Cross_Streets",
        theThing: "12th",
        url: "http://url2.com"
      },
      {
        id: 3,
        grouping1: "I124",
        grouping2: "Cross_Streets",
        theThing: "13th",
        url: "http://url3.com"
      },
      {
        id: 4,
        grouping1: "I124",
        grouping2: "Cross_Streets",
        theThing: "4th",
        url: "http://url4.com"
      },
      {
        id: 14,
        grouping1: "I124",
        grouping2: "Ramps",
        theThing: "Ramp_A",
        url: "http://url14.com"
      },
      {
        id: 15,
        grouping1: "I124",
        grouping2: "Ramps",
        theThing: "Ramp_B",
        url: "http://url15.com"
      },
      {
        id: 41,
        grouping1: "I75",
        grouping2: "Cross_Streets",
        theThing: "100th",
        url: "http://url41.com"
      }
    ];
    
    const resultAsObject = DATA.reduce((accumulator, item) => {
      const groupId = item["grouping1"];
      const subGroupId = item["grouping2"];
      const subGroupItemId = item["theThing"];
      const url = item["url"];
    
      const group = accumulator[groupId] || {
        key: groupId.toLowerCase(),
        label: groupId,
        nodes: []
      };
      const subGroup = group.nodes.find(
        item => item.key === subGroupId.toLowerCase()
      ) || {
        key: subGroupId.toLowerCase(),
        label: subGroupId,
        nodes: []
      };
      const updatedSubGroupNodes = [
        {
          key: subGroupItemId.toLowerCase(),
          label: subGroupItemId,
          url: url,
          nodes: []
        }
      ];
      const updatedSubGroup = {
        ...subGroup,
        nodes: updatedSubGroupNodes
      };
      const t1 = [...group.nodes, updatedSubGroup].reduce((acc, i) => {
        const category = acc.find(t => t.key === i.key) || {
          key: i.key,
          label: i.label,
          nodes: []
        };
        const updatedNodes = [...category.nodes, ...i.nodes];
        const updatedCategory = { ...category, nodes: updatedNodes };
    
        // replace the existing category object and append
        // the updated object with populated `nodes` property
        return [...acc.filter(t => t.key !== category.key), updatedCategory];
      }, []);
    
      const updatedGroup = {
        ...group,
        nodes: t1
      };
      return {
        ...accumulator,
        [groupId]: updatedGroup
      };
    }, {});
    
    console.log(resultAsObject);