Zamurph
Zamurph

Reputation: 89

How can you create a Tree display using dynamic data from an API in React?

So, I've got a bit of a doozy here. I've looked into trees and the like in react, and I'm fairly confident I can implement one, given the right data structure. The problem I'm running into is that I'm getting my data from an API that doesn't, at least natively, have the structure for a tree, so I'm trying to create that structure on the fly.

This is what the data I'm getting back from the API looks like:

const category = //could have children
{
  "object_name":"B2B",
  "data_provider_key":"bluekai",
  "object_key": "bluekai-31",
  "object_type":"category",
};

const segment = //will only be a child
{ 
  "object_name":"B2B > Role/Title > Admin Exec",
  "data_provider_key":"bluekai",
  "object_key": "bluekai-1145",
  "object_type":"segment",
  "cpm_cost":2.500
};

And this is the logic that I'm using to try and manipulate the data from the API to add children/create parents, etc.

const asyncView = async function (segTree: string | undefined) {

  const categoryDataCall = api.getBeeswaxSegmentView(categoryBody);
  const segmentDataCall = api.getBeeswaxSegmentView(segmentBody);

  const data = await Promise.all([categoryDataCall, segmentDataCall]);

  const parent = categoryData.find( (el: any) => el.object_key === segTree);

  const categories = data[0].payload;

  if (categories.length >= 1) {
    for (let i = 0; i < categories.length; i++) {
      categories[i].children = [];
    }
  }

  parent.children = categories.concat(data[1].payload); 

  setCategoryData(parent.children);
  setParent(parent);

}

asyncView(e.currentTarget.dataset.segment_tree);

}

return (
  <>
    <div>PARENT: {parent.object_name}</div>
    {categoryData.length === 0
      ? <div>No category data</div>
      : categoryData.map((e: any) => {
          if (e.object_type === 'segment') {
            return (
              <div data-segment_tree={`${e.object_key || "NULL"}`}
                data-provider_key={`${e.data_provider_key}`}
              >
               {`Name: ${e.object_name} (${e.object_key}, $${parseFloat(e.cpm_cost).toFixed(2)} CPM)`}
              </div>
            )
          }

          return (
            <div data-segment_tree={`${e.object_key || "NULL"}`}
              data-provider_key={`${e.data_provider_key}`}
              onClick={getCategoryAndSegmentData}
            >
              {`Name: ${e.data_provider_name || e.object_name}`}
            </div>
          )
        })
    }
  </>
);
}

I haven't implemented the Tree part yet, but that's because I am fairly confident I'm not creating the relations between elements correctly in my logic/the logic breaks if there are multiple 'trees'/categories on a page (which there will be.)

Sorry if this is a bit much, but any help or just ideas on dynamically modifying the data from the API to fit the tree structure of child/parent relationships would be appreciated!

Edit in response to Ray Hatfield:

What's the relationship between a category and a segment?

Segments will always be children of Categories, and will never have children of their own. Categories can have other categories as children.

How do you establish which category a segment belongs to?

The object_key property from the Category object gets passed to the API call(s) (two calls are made: one for segments, and one for categories). This is the only relation between segments and categories - nothing else in the return data ties them together.

What is e?

I assume you mean in the e.currentTarget.dataset.segment_tree line.

e is the event object, which I'm using to create the queries and firing them off on click events. I'm storing the object_key in a data-attribute in the HTML, and then passing it to a handler to generate the categoryBody and segmentBody used in the asyncView() function.

For some reason I have to explicitly pass the e.currentTarget.dataset.segment_tree as an argument to the async function even though they're in the same scope, but all it's doing is allowing me to find the Category that was clicked in the existing array of data in state.

What is categoryData?

categoryData is the array of values ( that is currently in state. So, each time I hit the API I update category data to re-render everything.

Effectively, I'm finding the parent (category that was clicked) firing off the API calls to get all the subcategories/segments associated with the clicked categories object_key, and then adding a children prop to any incoming categories, and then setting the children of the last clicked element equal to the returned segments + categories, and then rendering.

Upvotes: 1

Views: 2754

Answers (1)

ray
ray

Reputation: 27245

I put together this working demo on jsfiddle. Here are the highlights:


The Core Idea

The core idea is a Category component that's responsible for loading and rendering its own segments and subcategories. The subcategories get rendered using the same Category component, resulting in a recursive tree structure.


The Category Component

const Category = ({item}) => {

  const [data, setData] = React.useState();
  const onClick = data
    ? () => setData(null) // discard data (collapse) on subsequent click
    : () => load(item.object_key).then(setData);

  return (
    <div className="category">
      <div
        className={`category-name ${data ? 'open' : ''}`}
        onClick={onClick}
      >
        {item.object_name}
      </div>

      {data && (
        <ul>
          { data.map((child, i) => (
            <li key={i}><Node item={child}/></li>
          ))}
        </ul>
      )}
    </div>
  )
}

This component takes a single item prop representing the category. The component expects item to have object_key and object_name fields, like the category object in your example.

Initially the component has no information other than what's in the item, so it renders the category's name with an onClick handler that makes API calls to fetch the category's children and then stores the result in the component's state:

const [data, setData] = React.useState();
const onClick = () => load(item.object_key).then(setData);

On the subsequent render the Category component renders its children (segments and subcategories) in addition to the category name. Subcategories are rendered using the same Category component, resulting in a recursive tree structure.


The Segment Component

const Segment = ({item: {object_name}}) => (
  <div className="segment">{object_name}</div>
);

Simple component for rendering segments. Just returns the segment name here, but you could of course expand it to do whatever you need it to do.


The Node Component

const Node = ({item}) => {
  const Cmp = item.object_type === 'category' ? Category : Segment;
  return <Cmp item={item} />;
};

Convenience component for rendering a <Segment /> or <Category /> for the given item according to its type.


The rest of the example code is just hand waving to simulate the API calls and generate mock data.


load function

const load = async (parentKey) => {
  const [categories, segments] = await Promise.all([
    mockApiRequest('category'),
    mockApiRequest('segment')
  ]);

  return [
    ...categories,
    ...segments
  ];
}

Given a category's object_key, this makes the api calls to get the segments and subcategories, merges and returns the results as a single array.


mockApiRequest

const mockApiRequest = (type) => (
  new Promise((resolve) => {
    setTimeout(() => resolve(fakeData(type)), 200);
  })
)

Simulates the API request. Waits 200ms before resolving with mock data.


fakeData

// generate mock response data
const fakeData = (type) => {
  // copy the list of names
  const n = [...names];

  // plucks a random name from the list
  const getName = () => (
    n.splice(Math.floor(Math.random() * n.length), 1)[0]
  );

  // generate and return an array of data
  return Array.from(
    {length: Math.floor(Math.random() * 5) + 1},
    (_, i) => ({
      ...samples[type],
      object_name: getName()
    })
  )
};

Generates mock category or segment data by copying the sample and choosing a random name.

Upvotes: 3

Related Questions