1

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.

4
  • Can you elaborate on the api data and some of your variables? What's the relationship between a category and a segment? How do you establish which category a segment belongs to? What is e? What is categoryData? Commented Feb 27, 2020 at 16:30
  • Sure! Added an edit to my post. :) Commented Feb 27, 2020 at 18:37
  • Sorry for the delay. Assuming you haven't already solved this I'll try to get back to it today. Commented Feb 28, 2020 at 20:13
  • No worries. I haven't solved it yet, and any help would be appreciated. :) Commented Feb 28, 2020 at 23:37

1 Answer 1

3

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.

Sign up to request clarification or add additional context in comments.

2 Comments

This is incredible! Thank you so much! I haven't implemented it yet, but I see no reason it shouldn't work, so I've marked as the right answer. I'll update when it's working, but in any case, this is a super detailed and awesome answer. I thoroughly appreciate it. :)
UPDATE: Implemented and working like a charm. Thank you again!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.