dgreene1 / react-accessible-treeview

A react component that implements the treeview pattern as described by the WAI-ARIA Authoring Practices.
https://dgreene1.github.io/react-accessible-treeview
MIT License
267 stars 37 forks source link

Checkbox Selection Performance Degrades with 2000+ Tree Nodes #195

Closed mukesh-1612 closed 1 week ago

mukesh-1612 commented 1 week ago

When loading a tree with 4000 children, selecting checkboxes becomes very slow. The performance seems to degrade due to the rendering of a large number of nodes, causing a noticeable delay when selecting or deselecting a checkbox.

Steps to Reproduce:

Create a tree with around 4000 children. Attempt to select a checkbox for one of the tree nodes. Observe the delay in response when the checkbox is selected or deselected.

Expected Behavior Checkbox selection should be fast and responsive, even with a large number of tree nodes.

Actual Behavior There is a significant delay in checkbox selection when dealing with 4000 children, which seems related to rendering performance.

Possible Solutions Is there a recommended solution to handle such a large number of children efficiently?

mukesh-1612 commented 1 week ago
import { useRef, useState } from 'react';
import { FaSquare, FaCheckSquare, FaMinusSquare } from 'react-icons/fa';
import { IoMdArrowDropright } from 'react-icons/io';
import { AiOutlineLoading } from 'react-icons/ai';
import TreeView from 'react-accessible-treeview';
import cx from 'classnames';
import './styles.css';

const initialData = [
  {
    name: '',
    id: 0,
    children: [1, 2, 3],
    parent: null,
  },
  {
    name: 'Fruits',
    children: [],
    id: 1,
    parent: 0,
    isBranch: true,
  },
  {
    name: 'Drinks',
    children: [4, 5],
    id: 2,
    parent: 0,
    isBranch: true,
  },
  {
    name: 'Vegetables',
    children: [],
    id: 3,
    parent: 0,
    isBranch: true,
  },
  {
    name: 'Pine colada',
    children: [],
    id: 4,
    parent: 2,
  },
  {
    name: 'Water',
    children: [],
    id: 5,
    parent: 2,
  },
];

const getChildData = () => {
  const arr = [];
  for (let i = 0; i <= 5000; i++) {
    const uid =
      Date.now().toString(36) + Math.random().toString(36).substring(2);
    arr.push({
      name: 'Another child Node',
      children: [],
      id: uid,
      parent: 3,
    });
  }

  return arr;
};

function MultiSelectCheckboxAsync() {
  const loadedAlertElement = useRef(null);
  const [data, setData] = useState(initialData);
  const [nodesAlreadyLoaded, setNodesAlreadyLoaded] = useState([]);

  const updateTreeData = (list, id, children) => {
    const data = list.map((node) => {
      if (node.id === id) {
        node.children = children.map((el) => {
          return el.id;
        });
      }
      return node;
    });
    return data.concat(children);
  };

  const onLoadData = ({ element }) => {
    return new Promise((resolve) => {
      if (element.children.length > 0) {
        resolve();
        return;
      }
      setData((value) => updateTreeData(value, element.id, getChildData()));
      resolve();
    });
  };

  const wrappedOnLoadData = async (props) => {
    const nodeHasNoChildData = props.element.children.length === 0;
    const nodeHasAlreadyBeenLoaded = nodesAlreadyLoaded.find(
      (e) => e.id === props.element.id
    );

    await onLoadData(props);

    if (nodeHasNoChildData && !nodeHasAlreadyBeenLoaded) {
      const el = loadedAlertElement.current;
      setNodesAlreadyLoaded([...nodesAlreadyLoaded, props.element]);
      el && (el.innerHTML = `${props.element.name} loaded`);

      // Clearing aria-live region so loaded node alerts no longer appear in DOM
      setTimeout(() => {
        el && (el.innerHTML = '');
      }, 5000);
    }
  };

  return (
    <>
      <div>
        <div
          className="visually-hidden"
          ref={loadedAlertElement}
          role="alert"
          aria-live="polite"
        ></div>
        <div
          className="checkbox"
          style={{
            maxHeight: '80vh',
            overflowY: 'scroll',
          }}
        >
          <TreeView
            data={data}
            aria-label="Checkbox tree"
            onLoadData={wrappedOnLoadData}
            multiSelect
            propagateSelect
            togglableSelect
            propagateSelectUpwards
            nodeRenderer={({
              element,
              isBranch,
              isExpanded,
              isSelected,
              isHalfSelected,
              getNodeProps,
              level,
              handleSelect,
              handleExpand,
            }) => {
              const branchNode = (isExpanded, element) => {
                return isExpanded && element.children.length === 0 ? (
                  <>
                    <span
                      role="alert"
                      aria-live="assertive"
                      className="visually-hidden"
                    >
                      loading {element.name}
                    </span>
                    <AiOutlineLoading
                      aria-hidden={true}
                      className="loading-icon"
                    />
                  </>
                ) : (
                  <ArrowIcon isOpen={isExpanded} />
                );
              };
              return (
                <div
                  {...getNodeProps({ onClick: handleExpand })}
                  style={{ marginLeft: 40 * (level - 1) }}
                >
                  {isBranch && branchNode(isExpanded, element)}
                  <CheckBoxIcon
                    className="checkbox-icon"
                    onClick={(e) => {
                      handleSelect(e);
                      e.stopPropagation();
                    }}
                    variant={
                      isHalfSelected ? 'some' : isSelected ? 'all' : 'none'
                    }
                  />
                  <span className="name">{element.name}</span>
                </div>
              );
            }}
          />
        </div>
      </div>
    </>
  );
}

const ArrowIcon = ({ isOpen, className }) => {
  const baseClass = 'arrow';
  const classes = cx(
    baseClass,
    { [`${baseClass}--closed`]: !isOpen },
    { [`${baseClass}--open`]: isOpen },
    className
  );
  return <IoMdArrowDropright className={classes} />;
};

const CheckBoxIcon = ({ variant, ...rest }) => {
  switch (variant) {
    case 'all':
      return <FaCheckSquare {...rest} />;
    case 'none':
      return <FaSquare {...rest} />;
    case 'some':
      return <FaMinusSquare {...rest} />;
    default:
      return null;
  }
};

export default MultiSelectCheckboxAsync;
mellis481 commented 1 week ago

Loading any page with thousands of checkboxes would have performance issues. Any type of control actually (select, input). That's an extreme amount that I see no legitimate reason to expect to work in our library or even in standard HTML. A user is not going to look at 4000 elements and shouldn't even presented with that many.

I would advise adding some filtering before rendering a tree to reduce the number of nodes to a reasonable amount.

KrishEnacton commented 1 week ago

Could you implement react-window virtualization technique to this package?

dgreene1 commented 1 week ago

@KrishEnacton that’s out of scope for this library. You’re welcome to fork if that’s what you want to do. Can you explain what use case could possibly benefit from 4000+ nodes?

btw, the library supports lazy loading.

just to be clear, this ticket is more of a “StackOverflow” styled question which we make clear in our issue creation flow that we don’t respond to. So don’t be surprised if responses to this thread slow down.

mukesh-1612 commented 1 week ago

https://github.com/user-attachments/assets/c6bac325-04d4-48fe-ae3f-99a7465494ff

This is the use case where we want to select lessons in a course. There may not be 2000+ nodes in a single course, but with 10 courses, each having 500 lessons, it is possible to reach that number.

We have implemented this, but we were not aware of the performance issue, as we thought virtualization had been handled. I have attached the video of the use case.

As you can see, the data is fetched asynchronously, but the content loads very slowly, and the selections are delayed. Other than that, the package is really awesome and easy to integrate with our code.

dgreene1 commented 1 week ago

Here are some solutions and then after this we won’t be giving free advice anymore for our volunteer based open source library.

  1. Utilize the async / lazy loading capabilities of this library (please don’t ask how to do them since they’re in the docs). This will make it so you only have the nodes you need when you need them.
  2. Change the user experience so that the courses are not the first level of the tree. Instead require the student to select their course first. After the course has been selected, you can load the tree of the attachments.

Ultimately a tree is not the best use case for what you’ve shown us.

We might lock this thread in the future. Please consider our recommendations as your request is not in scope for this library and won’t be done.