[SOLVED] Optimal way of transforming flat map to nested data structure using React.js?

Issue

I’ve been pondering the best way to handle grouping in my app. It’s a video editing app and I am introducing the ability to group layers. If you’re familiar with Figma or any design/video editing program then there is usually the ability to group layers.

To keep this simple in the app the video data is a map

const map = {
  "123": {
    uid: "123",
    top: 25,
    type: "text"
  },
  "345": {
    uid: "345",
    top: 5,
    type: "image"
  },
  "567": {
    uid: "567",
    top: 25,
    type: "group"
    children: ["345", "123"]
  }
}

Then I am grouping them inside a render function (this feels expensive)

const SomeComponent = () => {
  const objects = useMemo(() => makeTrackObjects(map), [map]);

  return (
    <div>
      {objects.map(object => {
        return <div>Some layer that will change the data causing re-renders</div>
      })}
    </div>
  )
}

Here is the function that does the grouping

const makeTrackObjects = (map) => {
  // converts map to array
  const objects = Object.keys(map).map((key: string) => ({ ...map[key] }));

  // flat array of all objects to be grouped by their key/id
  const objectsInGroup = objects
    .filter((object) => object.type === "group")
    .map((object) => object.children)
    .flat();

  // filter out objects that are nested/grouped
  const filtered = objects.filter((object) => !objectsInGroup.includes(object.uid))

  // insert objects as children during render
  const grouped = filtered.map((object) => {
      const children = object.children
        ? {
            children: object.children
              .map((o, i) => {
                return {
                  ...map[o]
                };
              })
              .flat()
          }
        : {};

      return {
        ...object,
        ...children
      };
    });

  // the core data is flat but now nested for the UI. Is this inefficient?
  return grouped

}

Ideally I would like to keep the data flat, I have a lot of code that I would have to update to go deep in the data. It feels nice to have it flat and transformers in certain areas where needed.

The main question is does this make sense, is it efficient, and if not then why?

Solution

If you are running into performance issues, one area you may want to investigate is how you are chaining array functions (map, filter, flat, etc). Each call to one of these functions creates an intermediate collection based on the array it receives. (For instance, if we chained 2 map functions, this is looping through the full array twice). You could increase performance by creating one loop and adding items into a collection. (Here’s an article that touches on this being a motivation for transducers.)

I haven’t encountered a performance issue with this before, but you may also want to remove spread (...) when unnecessary.

Here is my take on those adjustments on makeTrackObjects.

Update

I also noticed that you are using includes while iterating through an array. This is effectively O(n^2) time complexity because each item will be scanned against the full array. One way to mitigate is to instead use a Set to check if that content already exists, turning this into O(n) time complexity.

const map = {
  "123": {
    uid: "123",
    top: 25,
    type: "text"
  },
  "345": {
    uid: "345",
    top: 5,
    type: "image"
  },
  "567": {
    uid: "567",
    top: 25,
    type: "group",
    children: ["345", "123"]
  }
};

const makeTrackObjects = (map) => {
  // converts map to array
  const objects = Object.keys(map).map((key) => map[key]);

  // set of all objects to be grouped by their key/id
  const objectsInGroup = new Set();
  objects.forEach(object => {
    if (object.type === "group") {
      object.children.forEach(child => objectsInGroup.add(child));
    }
  });

  // filter out objects that are nested/grouped
  const filtered = objects.filter((object) => !objectsInGroup.has(object.uid))

  // insert objects as children during render
  const grouped = filtered.map((object) => {
    const children = {};

    if (object.children) {
      children.children = object.children.map(child => map[child]);
    }

    return {
      ...object,
      ...children
    };
  });

  // the core data is flat but now nested for the UI. Is this inefficient?
  return grouped

}

console.log(makeTrackObjects(map));

Answered By – GenericUser

Answer Checked By – Clifford M. (BugsFixing Volunteer)

Leave a Reply

Your email address will not be published. Required fields are marked *