0

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?

2
  • It's all about performance, if your data is really big, all manipulations of grouping and changing the data structure on the client is a bad decision. Ideally, prepare a comfortable data structure on the server. Otherwise, you will have to sacrifice performance for development comfort. Commented Jan 3, 2022 at 16:30
  • Right so it's better to not transform on the client Commented Jan 3, 2022 at 16:50

1 Answer 1

1

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));

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

3 Comments

This looks good, will love to try it. Im curious to know if its generally bad practice to transform data on the client side like this as a rule of thumb. Ill take a look at the article you shared as it may address this concern. Cheers
@MichaelJosephAubry I'd say that depends on the context - whether you have control of the server, volume of data, business requirements, etc. The browser is more capable than most people give it credit for, and it's not hard to find applications that do heavy data transformations client-side without user issues.
Cool, then I guess its safe to say as a rule of thumb to avoid performance issues its important to stick with 0(n) as much as possible -- would be cool to have a react dev tool that checks if your function is O(n) during testing. Then to benchmark on a per use case basis.

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.