Giter Site home page Giter Site logo

lodin / react-vtree Goto Github PK

View Code? Open in Web Editor NEW
367.0 5.0 41.0 7.11 MB

React component for efficiently rendering large tree structures

Home Page: https://lodin.github.io/react-vtree/

License: MIT License

JavaScript 2.59% TypeScript 97.41%
react react-components windowing virtualization tree performance react-window

react-vtree's Introduction

react-vtree

Latest Stable Version License CI Status Coverage Bugs Vulnerabilities

This package provides a lightweight and flexible solution for rendering large tree structures. It is built on top of the react-window library.

Attention! This library is entirely rewritten to work with the react-window. If you are looking for the tree view solution for the react-virtualized, take a look at react-virtualized-tree.

NOTE: This is the documentation for version 3.x.x. For version 2.x.x see this branch.

Installation

# npm
npm i react-window react-vtree

# Yarn
yarn add react-window react-vtree

Usage

FixedSizeTree

Example

You can also take a look at the very similar example at the Storybook:

import {FixedSizeTree as Tree} from 'react-vtree';

// Tree component can work with any possible tree structure because it uses an
// iterator function that the user provides. Structure, approach, and iterator
// function below is just one of many possible variants.
const treeNodes = [
  {
    name: 'Root #1',
    id: 'root-1',
    children: [
      {
        children: [
          {id: 'child-2', name: 'Child #2'},
          {id: 'child-3', name: 'Child #3'},
        ],
        id: 'child-1',
        name: 'Child #1',
      },
      {
        children: [{id: 'child-5', name: 'Child #5'}],
        id: 'child-4',
        name: 'Child #4',
      },
    ],
  },
  {
    name: 'Root #2',
    id: 'root-2',
  },
];

// This helper function constructs the object that will be sent back at the step
// [2] during the treeWalker function work. Except for the mandatory `data`
// field you can put any additional data here.
const getNodeData = (node, nestingLevel) => ({
  data: {
    id: node.id.toString(), // mandatory
    isLeaf: node.children.length === 0,
    isOpenByDefault: true, // mandatory
    name: node.name,
    nestingLevel,
  },
  nestingLevel,
  node,
});

// The `treeWalker` function runs only on tree re-build which is performed
// whenever the `treeWalker` prop is changed.
function* treeWalker() {
  // Step [1]: Define the root node of our tree. There can be one or
  // multiple nodes.
  for (let i = 0; i < treeNodes.length; i++) {
    yield getNodeData(treeNodes[i], 0);
  }

  while (true) {
    // Step [2]: Get the parent component back. It will be the object
    // the `getNodeData` function constructed, so you can read any data from it.
    const parent = yield;

    for (let i = 0; i < parent.node.children.length; i++) {
      // Step [3]: Yielding all the children of the provided component. Then we
      // will return for the step [2] with the first children.
      yield getNodeData(parent.node.children[i], parent.nestingLevel + 1);
    }
  }
}

// Node component receives all the data we created in the `treeWalker` +
// internal openness state (`isOpen`), function to change internal openness
// state (`setOpen`) and `style` parameter that should be added to the root div.
const Node = ({data: {isLeaf, name}, isOpen, style, setOpen}) => (
  <div style={style}>
    {!isLeaf && (
      <button type="button" onClick={() => setOpen(!isOpen)}>
        {isOpen ? '-' : '+'}
      </button>
    )}
    <div>{name}</div>
  </div>
);

ReactDOM.render(
  <Tree treeWalker={treeWalker} itemSize={30} height={150} width={300}>
    {Node}
  </Tree>,
  document.querySelector('#root'),
);

Props

Props inherited from FixedSizeList

You can read more about these properties in the FixedSizeList documentation.

  • children: component. Uses own implementation, see below.
  • className: string = ""
  • direction: strig = "ltr"
  • height: strig | number
  • initialScrollOffset: number = 0
  • innerRef: function | createRef object. This property works as it described in the react-window. For getting a FixedSizeList reference use listRef.
  • innerElementType: React.ElementType = "div"
  • innerTagName: string. Deprecated by react-window.
  • itemData: any
  • itemKey: function. Handled internally.
  • itemSize: number
  • layout: string = "vertical"
  • onItemsRendered: function
  • onScroll: function
  • outerRef: function | createRef object
  • outerElementType: React.ElementType = "div"
  • outerTagName: string. Deprecated by react-window.
  • overscanCount: number = 1
  • style: object = null
  • useIsScrolling: boolean = false
  • width: number | string
async: boolean

This option allows making the tree asynchronous; e.g. you will be able to load the branch data on the node opening. All it does under the hood is preserving the tree state between tree buildings on treeWalker update, so the user does not see the tree resetting to the default state when the async action is performed.

To see how it works you can check the AsyncData story. You can use the disableAsync to see what will happen on the async action if the async prop is false.

If it is combined with the placeholder option, the tree re-building won't be interrupted by showing the placeholder; it will be shown only at the first time the tree is building.

To see how two options interact with each other see the AsyncDataIdle story.

children: component

The Node component responsible for rendering each node.

It receives the following props:

  • Inherited from react-window's Row component:

    • style: object
    • isScrolling: boolean - if useIsScrolling is enabled.
  • Node-specific props:

    • All fields of the FixedSizeNodePublicState object.
    • treeData: any - any data provided via the itemData property of the FixedSizeTree component.
placeholder: ReactNode | null

This property receives any react node that will be displayed instead of a tree during the building process. This option should only be used if the tree building process requires too much time (which means you have a really giant amount of data, e.g. about a million nodes).

Setting this option enables the requestIdleCallback under the hood for browsers that support this feature. For other browsers the original scenario is applied; no placeholder will be shown.

Using this feature allows avoiding UI freezes; however, it may slightly increase the time spent for the building process.

To see how it works, you can check the BigData story. Use placeholder tool to add and remove placeholder.

If you have an asynchronous giant tree and want to use profits of requestIdleCallback but don't want placeholder to be shown on the first render (that is probably quite small because all other data will be loaded asynchronously), set placeholder to null. No placeholder will be shown on the first render but the requestIdleCallback building will be enabled and allow avoiding freezes on tree re-building when tree becomes bigger.

To see how it works you can check the AsyncDataIdle story. It uses the null placeholder, so no text is shown for the first build but async requests don't block the UI.

buildingTaskTimeout: number

This option works in tandem with the placeholder option. With it, you can set the task timeout for the requestIdleCallback. The buildingTaskTimeout will be sent directly as the requestIdleCallback's timeout option.

listRef: Ref<FixedSizeList>

This option allows you to get the instance of the internal react-window list. It is usually unnecessary because all necessary methods are already provided but still can be useful for edge cases.

rowComponent: component

This property receives a custom Row component for the FixedSizeList that will override the default one. It can be used for adding new functionality.

Row component receives the following props:

  • index: number
  • data: object - the data tree component provides to Row. It contains the following data:
    • component: component - a Node component to create a React element from.
    • getRecordData: function - a function that gets the record data by index. It returns a FixedSizeNodePublicState object.
    • treeData: any - any data provided via the itemData property of the FixedSizeTree component.
  • style: object
  • isScrolling: boolean
* treeWalker()

An iterator function that walks around the tree and yields node data to build an inner representation of the tree. For algorithm details, see TreeWalker section.

The treeWalker function should yield the object of the following shape:

  • data: FixedSizeNodeData - this field is mandatory. See FixedSizeNodeData type for the shape.
  • ... - you can add any other data to this object. It will be sent directly to the treeWalker at the step [2] of the execution.

Tree is re-computed on each treeWalker change. To avoid unnecessary tree re-computation keep the treeWalker memoized (e.g. with useCallback hook). If you want to update tree data, send the new version of treeWalker to the tree component.

Note that when treeWalker is updated no internal state will be shared with the new tree. Everything will be built from scratch.

Methods

The component provides the following methods.

scrollToItem(id: string | symbol, align?: Align): void

The scrollToItem method behaves the same as scrollToItem from FixedSizeList but receives node id instead of index.

async recomputeTree(state): void

This method starts the tree traversing to update the internal state of nodes.

It receives state object that contains nodes' id as keys and update rules as values. Each record traverses a subtree of the specified node (also "owner node") and does not affect other nodes (it also means that if you specify the root node the whole tree will be traversed).

The rules object has the following shape:

  • open: boolean - this rule changes the openness state for the owner node only (subtree nodes are not affected).
  • subtreeCallback(node: object, ownerNode: object): void - this callback runs against each node in the subtree of the owner node (including the owner node as well). It receives the subtree node and the owner node. Changing any property of the subtree node will affect the node state and how it will be displayed (e.g. if you change the node openness state it will be displayed according to the changed state).

The order of rules matters. If you specify the child node rules before the parent node rules, and that rules affect the same property, the parent node subtreeCallback will override that property. So if you want to override parent's rules, place children rules after the parent's.

The type of the node objects received by subtreeCallback is FixedSizeNodePublicState. See the types description below.

recomputeTree example
// The tree
const tree = {
  name: 'Root #1',
  id: 'root-1',
  children: [
    {
      children: [
        {id: 'child-2', name: 'Child #2'},
        {id: 'child-3', name: 'Child #3'},
      ],
      id: 'child-1',
      name: 'Child #1',
    },
    {
      children: [{id: 'child-5', name: 'Child #5'}],
      id: 'child-4',
      name: 'Child #4',
    },
  ],
};

// recomputeTree

tree.recomputeTree({
  'root-1': {
    open: false,
    subtreeCallback(node, ownerNode) {
      // Since subtreeCallback affects the ownerNode as well, we can check if the
      // nodes are the same, and run the action only if they aren't
      if (node !== ownerNode) {
        // All nodes of the tree will be closed
        node.isOpen = false;
      }
    },
  },
  // But we want `child-4` to be open
  'child-4': true,
});

Types

  • FixedSizeNodeData - value of the data field of the object yielded by the treeWalker function. The shape is the following:
    • id - a unique identifier of the node.
    • isOpenByDefault - a default openness state of the node.
    • ... - you can add any number of additional fields to this object. This object without any change will be sent directly to the Node component. You can also use getRecordData function to get this object along with the other record data by the index. To describe that data, you have to create a new type that extends the FixedSizeNodeData type.
  • FixedSizeNodePublicState<TData extends FixedSizeNodeData> - the node state available for the Node component and recomputeTree's subtreeCallback function. It has the following shape:
    • data: FixedSizeNodeData.
    • isOpen: boolean - a current openness status of the node.
    • setOpen(state: boolean): function - a function to change the openness state of the node. It receives the new openness state as a boolean and opens/closes the node accordingly.
  • FixedSizeTreeProps<TData extends FixedSizeNodeData> - props that FixedSizeTree component receives. Described in the Props section.
  • FixedSizeTreeState<TData extends FixedSizeNodeData> - state that FixedSizeTree component has.

VariableSizeTree

Example

You can also take a look at the very similar example at the Storybook:

import {VariableSizeTree as Tree} from 'react-vtree';

// Tree component can work with any possible tree structure because it uses an
// iterator function that the user provides. Structure, approach, and iterator
// function below is just one of many possible variants.
const tree = {
  name: 'Root #1',
  id: 'root-1',
  children: [
    {
      children: [
        {id: 'child-2', name: 'Child #2'},
        {id: 'child-3', name: 'Child #3'},
      ],
      id: 'child-1',
      name: 'Child #1',
    },
    {
      children: [{id: 'child-5', name: 'Child #5'}],
      id: 'child-4',
      name: 'Child #4',
    },
  ],
};

// This helper function constructs the object that will be sent back at the step
// [2] during the treeWalker function work. Except for the mandatory `data`
// field you can put any additional data here.
const getNodeData = (node, nestingLevel) => ({
  data: {
    defaultHeight: itemSize, // mandatory
    id: node.id.toString(), // mandatory
    isLeaf: node.children.length === 0,
    isOpenByDefault: true, // mandatory
    name: node.name,
    nestingLevel,
  },
  nestingLevel,
  node,
});

// The `treeWalker` function runs only on tree re-build which is performed
// whenever the `treeWalker` prop is changed.
function* treeWalker() {
  // Step [1]: Define the root node of our tree. There can be yielded one or
  // multiple nodes.
  yield getNodeData(tree, 0);

  while (true) {
    // Step [2]: Get the parent component back. It will be the object
    // the `getNodeData` function constructed, so you can read any data from it.
    const parent = yield;

    for (let i = 0; i < parent.node.children.length; i++) {
      // Step [3]: Yielding all the children of the provided component. Then we
      // will return for the step [2] with the first children.
      yield getNodeData(parent.node.children[i], parent.nestingLevel + 1);
    }
  }
}

// Node component receives current node height as a prop
const Node = ({data: {isLeaf, name}, height, isOpen, style, setOpen}) => (
  <div style={style}>
    {!isLeaf && (
      <button type="button" onClick={() => setOpen(!isOpen)}>
        {isOpen ? '-' : '+'}
      </button>
    )}
    <div>{name}</div>
  </div>
);

const Example = () => (
  <Tree treeWalker={treeWalker} height={150} width={300}>
    {Node}
  </Tree>
);

Props

Props inherited from VariableSizeList

You can read more about these properties in the VariableSizeList documentation.

Since VariableSizeList in general inherits properties from the FixedSizeList, everything described in the same section for FixedSizeTree affects this section. For the rest, there are the following changes:

  • estimatedItemSize: number = 50
  • itemSize: (index: number) => number. This property is optional. If it is not provided, the defaultHeight of the specific node will be used. Advanced property; prefer using node state for it.
children

The Node component. It is the same as the FixedSizeTree's one but receives properties from the VariableSizeNodePublicState object.

listRef: Ref<VariableSizeList>

Same as listRef of FixedSizeTree.

rowComponent: component

See rowComponent in the FixedSizeTree section; the getRecordData returns the VirtualSizeNodePublicState object.

* treeWalker(refresh: boolean)

An iterator function that walks over the tree. It behaves the same as FixedSizeTree's treeWalker. The data object should be in the VariableSizeNodeData shape.

Methods

The component provides the following methods:

scrollToItem(id: string | symbol, align?: Align): void

The scrollToItem method behaves the same as scrollToItem from VariableSizeList but receives node id instead of index.

resetAfterId(id: string | symbol, shouldForceUpdate: boolean = false): void

This method replaces the resetAfterIndex method of VariableSizeList but works exactly the same. It receives node id as a first argument.

async recomputeTree(state): void

See FixedSizeTree's recomputeTree description. There are no differences.

Types

All types in this section are the extended variants of FixedSizeTree types.

  • VariableSizeNodeData - this object extends FixedSizeNodeData and contains the following additional fields:
    • defaultHeight: number - the default height the node will have.
  • VariableSizeNodePublicState<TData extends VariableSizeNodeData>. The node state object. Extends the FixedSizeNodePublicState and contains the following additional fields:
    • height: number - the current height of the node. The node will be displayed with this height.
    • resize(newHeight: number, shouldForceUpdate?: boolean): function - a function to change the height of the node. It receives two parameters:
      • newHeight: number - a new height of the node.
      • shouldForceUpdate: boolean - an optional argument that will be sent directly to the resetAfterIndex method.
  • VariableSizeTreeProps<T extends VariableSizeNodeData>.
  • VariableSizeTreeState<T extends VariableSizeNodeData>.

TreeWalker algorithm

The treeWalker algorithm works in the following way. During the execution, the treeWalker function sends a bunch of objects to the tree component which builds an internal representation of the tree. However, for it, the specific order of yieldings should be performed.

  1. The first yielding is always root nodes. They will be the foundation of the whole tree.
  2. Now start a loop where you will receive the parent node and yield all the children of it.
  3. The first yielding of loop iteration should yield an undefined. In exchange, you will receive a node for which you should yield all the children in the same way you've done with the root ones.
  4. When all the children are yielded, and the new iteration of loop is started, you yield undefined again and in exchange receive the next node. It may be:
    • a child node if the previous node has children;
    • a sibling node if it has siblings;
    • a sibling of the elder node.
  5. When the whole tree is finished and algorithm reaches the end, the loop stops. You don't have to finish treeWalker's loop manually.

The example of this algorithm is the following treeWalker function:

function* treeWalker() {
  // Here we start our tree by yielding the data for the root node.
  yield getNodeData(rootNode, 0);

  while (true) {
    // Here in the loop we receive the next node whose children should be
    // yielded next.
    const parent = yield;

    for (let i = 0; i < parent.node.children.length; i++) {
      // Here we go through the parent's children and yield them to the tree
      // component
      yield getNodeData(parent.node.children[i], parent.nestingLevel + 1);
      // Then the loop iteration is over, and we are going to our next parent
      // node.
    }
  }
}

Migrating 2.x.x -> 3.x.x

If you use react-vtree of version 2, it is preferable migrate to the version 3. The third version is quite different under the hood and provides way more optimized approach to the initial tree building and tree openness state change. The most obvious it becomes if you have a giant tree (with about 1 million of nodes).

To migrate to the new version, you have to do the following steps.

1. Migrate treeWalker

The treeWalker was and is the heart of the react-vtree. However, now it looks a bit different.

Old treeWalker worked for both initial tree building and changing node openness state:

function* treeWalker(refresh) {
  const stack = [];

  stack.push({
    nestingLevel: 0,
    node: rootNode,
  });

  // Go through all the nodes adding children to the stack and removing them
  // when they are processed.
  while (stack.length !== 0) {
    const {node, nestingLevel} = stack.pop();
    const id = node.id.toString();

    // Receive the openness state of the node we are working with
    const isOpened = yield refresh
      ? {
          id,
          isLeaf: node.children.length === 0,
          isOpenByDefault: true,
          name: node.name,
          nestingLevel,
        }
      : id;

    if (node.children.length !== 0 && isOpened) {
      for (let i = node.children.length - 1; i >= 0; i--) {
        stack.push({
          nestingLevel: nestingLevel + 1,
          node: node.children[i],
        });
      }
    }
  }
}

The new treeWalker is only for the tree building. The Tree component builds and preserves the tree structure internally. See the full description above.

// This function prepares an object for yielding. We can yield an object
// that has `data` object with `id` and `isOpenByDefault` fields.
// We can also add any other data here.
const getNodeData = (node, nestingLevel) => ({
  data: {
    id: node.id.toString(),
    isLeaf: node.children.length === 0,
    isOpenByDefault: true,
    name: node.name,
    nestingLevel,
  },
  nestingLevel,
  node,
});

function* treeWalker() {
  // Here we send root nodes to the component.
  for (let i = 0; i < rootNodes.length; i++) {
    yield getNodeData(rootNodes[i], 0);
  }

  while (true) {
    // Here we receive an object we created via getNodeData function
    // and yielded before. All we need here is to describe its children
    // in the same way we described the root nodes.
    const parentMeta = yield;

    for (let i = 0; i < parentMeta.node.children.length; i++) {
      yield getNodeData(
        parentMeta.node.children[i],
        parentMeta.nestingLevel + 1,
      );
    }
  }
}

2. Migrate tree components

Components haven't been changed a lot but you may want to add new features like:

3. Migrate recomputeTree method

The recomputeTree method now receives a list of nodes to change (previously, it was an opennessState object). See the full description above.

The most important change is the introduction of the subtreeCallback. It is a function that will be applied to each node in the subtree of the specified node. Among other useful things it also allows imitating the behavior of old useDefaultOpenness and useDefaultHeight options.

Old recomputeTree:

treeInstance.recomputeTree({
  opennessState: {
    'node-1': true,
    'node-2': true,
    'node-3': false,
  },
  refreshNodes: true,
  useDefaultOpenness: false,
});

New recomputeTree:

treeInstance.recomputeTree({
  'node-1': true,
  'node-2': {
    open: true,
    subtreeCallback(node, ownerNode) {
      if (node !== ownerNode) {
        node.isOpen = false;
      }
    },
  },
  'node-3': false,
});

4. Migrate all your toggle() calls to setOpen(boolean)

In the 3.x.x version node provides a setOpen function instead of toggle that allows more fine-grained control over the openness state.

Old toggle:

const Node = ({data: {isLeaf, name}, isOpen, style, toggle}) => (
  <div style={style}>
    {!isLeaf && (
      <div>
        <button onClick={toggle}>{isOpen ? '-' : '+'}</button>
      </div>
    )}
    <div>{name}</div>
  </div>
);

New setOpen:

const Node = ({data: {isLeaf, name}, isOpen, style, setOpen}) => (
  <div style={style}>
    {!isLeaf && (
      <div>
        // Imitating the old `toggle` function behavior
        <button onClick={() => setOpen(!isOpen)}>{isOpen ? '-' : '+'}</button>
      </div>
    )}
    <div>{name}</div>
  </div>
);

5. Migrate all your IDs to string

Using node IDs as keys should improve React rendering performance. However, it means that you won't be able to use Symbol as IDs anymore. You should move all your IDs to be strings instead of symbols.

react-vtree's People

Contributors

heavensregent avatar johannalee avatar lodin avatar lukasbuenger avatar ptoussai avatar sirgallifrey avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

react-vtree's Issues

VariableSizeTree height does not behave as expected from upgrading to v2.0.0

I have a use case where the parent's height is different from the children's height. This was working fine in v1.0.2. However, after upgrading to v2.0.0 all the elements seem to be the same as the parent's height.

After some investigation, it looks like getItemSize() is not being called after toggle. This is due to lastMeasuredIndex in VariableSizeList in react-window is not reset to -1 after children are added to the records (which is the case in the v1.0.2).

Adding resetAfterId(record.data.id, true) in toggle() resolves the issue. Just wondering if there is a proper fix for this issue. I can provide code examples if this is not clear. Thanks!

Opening a node programatically

How would you open a node programatically? I'm using a scrollToItem method and I would like to open the parent nodes if the item is not visible. I would map the parent ids to the item id, so just need to open the nodes based on the ids I have.

Good job with the library!

React VTree example is not working

I am trying to use example given in dcoumentation of vtree but that does not seem to work

import { FixedSizeTree as Tree } from 'react-vtree';

// Tree component can work with any possible tree structure because it uses an
// iterator function that the user provides. Structure, approach, and iterator
// function below is just one of many possible variants.
const tree = {
name: 'Root #1',
id: 'root-1',
children: [
{
children: [
{ id: 'child-2', name: 'Child #2' },
{ id: 'child-3', name: 'Child #3' },
],
id: 'child-1',
name: 'Child #1',
},
{
children: [{ id: 'child-5', name: 'Child #5' }],
id: 'child-4',
name: 'Child #4',
},
],
};

function* treeWalker(refresh: any): any {
const stack = [];

// Remember all the necessary data of the first node in the stack.
stack.push({
    nestingLevel: 0,
    node: tree,
});

// Walk through the tree until we have no nodes available.
while (stack.length !== 0) {
    // const {
    //   node: {children = [], id, name},
    //   nestingLevel,
    // } = stack.pop();
    var removed : any = stack.pop();
    let children = removed.children;
    let id = removed.id;
    let name = removed.name;
    let nestingLevel = removed.nestingLevel;
    // Here we are sending the information about the node to the Tree component
    // and receive an information about the openness state from it. The
    // `refresh` parameter tells us if the full update of the tree is requested;
    // basing on it we decide to return the full node data or only the node
    // id to update the nodes order.
    const isOpened = yield refresh
        ? {
            id,
            isLeaf:children && children.length === 0,
            isOpenByDefault: true,
            name,
            nestingLevel,
        }
        : id;

    // Basing on the node openness state we are deciding if we need to render
    // the child nodes (if they exist).
    if (children && children.length !== 0 && isOpened) {
        // Since it is a stack structure, we need to put nodes we want to render
        // first to the end of the stack.
        for (let i = children.length - 1; i >= 0; i--) {
            stack.push({
                nestingLevel: nestingLevel + 1,
                node: children[i],
            });
        }
    }
}

}

// Node component receives all the data we created in the treeWalker +
// internal openness state (isOpen), function to change internal openness
// state (toggle) and style parameter that should be added to the root div.
const Node = (props: any) => (

<div style={props.style}>
    
    {!props.data.isLeaf && (
        <button type="button" onClick={props.toggle}>
            {props.isOpen ? '-' : '+'}
        </button>
    )}
    <div>{props.data.name}</div>
</div>

);

const CustomTree = () => (

{Node}

);
export default CustomTree

Below are the version I am using

"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-scripts": "4.0.3",
"react-vtree": "^2.0.4",
"react-window": "^1.8.6",

Any help , appreciated .

Build fails with Typescript version v4.4

TS v4.4 brought in modifications to lib dom type defintions (reference).

These conflict with the types declared for requestIdleCallback and cancelIdleCallback on global window here:

declare global {
  const requestIdleCallback: (
    callback: (deadline: RequestIdleCallbackDeadline) => void,
    opts?: RequestIdleCallbackOptions,
  ) => RequestIdleCallbackHandle;
  const cancelIdleCallback: (handle: RequestIdleCallbackHandle) => void;

  // eslint-disable-next-line @typescript-eslint/consistent-type-definitions
  interface Window {
    requestIdleCallback: typeof requestIdleCallback;
    cancelIdleCallback: typeof cancelIdleCallback;
  }
}

The above results in below errors during build:

node_modules/react-vtree/dist/es/utils.d.ts:16:11 - error TS2451: Cannot redeclare block-scoped variable 'requestIdleCallback'.
16     const requestIdleCallback: (callback: (deadline: RequestIdleCallbackDeadline) => void, opts?: RequestIdleCallbackOptions) => RequestIdleCallbackHandle;
             ~~~~~~~~~~~~~~~~~~~
  node_modules/typescript/lib/lib.dom.d.ts:17772:18
    17772 declare function requestIdleCallback(callback: IdleRequestCallback, options?: IdleRequestOptions): number;
                           ~~~~~~~~~~~~~~~~~~~
    'requestIdleCallback' was also declared here.

node_modules/react-vtree/dist/es/utils.d.ts:17:11 - error TS2451: Cannot redeclare block-scoped variable 'cancelIdleCallback'.
17     const cancelIdleCallback: (handle: RequestIdleCallbackHandle) => void;
             ~~~~~~~~~~~~~~~~~~
  node_modules/typescript/lib/lib.dom.d.ts:17741:18
    17741 declare function cancelIdleCallback(handle: number): void;
                           ~~~~~~~~~~~~~~~~~~
    'cancelIdleCallback' was also declared here.

I am unable to use later TS version because of this error, can we drop the redundant type declarations in favor of native support (I don't think upgrading TS version breaks anything within the library)?
Alternatively can someone suggest how can I override types here as a workaround?

scroll to item as a prop

Hi @Lodin, can we add a initialScrollToItem prop (which takes an id) in addition to scrollToItem method. The context is, I'm using this react-vtree in a popover component, which shows a list of items, and when the user clicks on it, the id of that node is remembered. So when we again open a tree I want to bring that node into the visible area.

Toggling OpennessState of Items with different nestingLevels must be done sequentially

In my project I am attempting to open all parent nodes to reveal a target node. In my initial approach I reduced an array of node IDs into an object to pass to recomputeTree. I found this never worked, and I must recompute the tree multiple times in order of nesting level to successfully expand all the nodes in my array.

Here is a link to a sandbox example.
https://codesandbox.io/s/cold-field-7q3kr?file=/src/Tree.tsx

Notice the first node in my list of target IDs to expand does correctly open, but not the second target ID.
I've included a block of code that is commented out, that does successfully open all the nodes listed in the array of IDs, but I didn't expect this to be needed, and it calls recomputeTree once for every ID in the list.

data.resize(0, true) does not update node after top position

Currently in my Node function, I have to check if node has been flagged for removal which I pass along with the node props that are yielded in the tree walker. That all works and is able to change the height of one of the nodes to 0 but unfortunately, when I call the data.resize(), which appears to change the 'top' position of the next node, it does not appear to do anything.

Thanks! Hope that makes sense!
Screen Shot 2020-02-20 at 5 10 41 PM

Reduce necessity of tree full tree walk

I was curious if you had considered any performance optimizations around the need for a full tree walk. I think one of the great parts of the iterator model is that you have the ability to progressively walk the tree as the user scrolls.

Couple this with the consistent height of elements, and it's possible to calculate what element you need to start iterating from to service any request.

Curious if performance optimizations like those were ever intended to be a part of react-vtree or if that's not the problem you're trying to solve.

How to update tree after dynamically modifying nodes height?

Hi there! Thanks for offering that component, couldn't live without it!

I've been using it for a while without any roadblocks, however I'm trying to add a new feature:

I have a tree where nodes have thumbnails, looks as follow:

Screenshot 2022-11-06 at 17 03 47

I am now implementing a slider to control the size of the thumbnails, so basically node heights will depend of the value entered from the slider. I am passing a prop to the tree that is that height, used in node height computation. It works excepted that expanded nodes aren't being updated dynamically:

Screenshot 2022-11-06 at 17 04 00

To see the update I have to manually collapse/expand the parent:

Screenshot 2022-11-06 at 17 04 14

Any way to force the tree to re-render with correct height? Thanks for any insights.

I didn't find any where in the readme about sorting & filter feature ?

Hi @Lodin ,

I am working on the project. So the problem here are -

  1. Rendering of giant data of nodes in tree (approx. 2k-3k)
  2. Have filter & sorting feature
  3. Performance issue of page due to such huge size of data

However, in the doc, I didn't find anywhere how to tackle with sorting & filtering feature. It would be really great, if you can direct me to the doc where these things are mentioned ?

Allow async tree walkers?

Example use case: I’m reading object data from an async source. I could optimize initial load time by only reading data for top-level nodes at first. Then, when a node is opened, I would fetch child node data by querying my async source—however, that means the walker must be asynchronous.

The key point I think is potential await iter.next() around here. Tree computer might then have to become async as well, and there would need to be some logic for handling delayed opening of nodes (with some sort of “is loading” boolean prop?).

I wonder whether async tree walkers are on the radar, or perhaps I’m missing some way of addressing this challenge that doesn’t require asynchronous walkers🤔

Is it possible to dynamically determine node size and provide it to vtree?

Heya 👋

First of all, sorry if I'm asking a known question, I'm just not too familiar with this library.

I've a tree structure whose nodes can have different heights. I'm not sure if I can know each height easily though. For some nodes I can calculate it just by knowing the content since they are text. But other nodes have things like embed content that can have hard to determine heights.

react-virtualized has CellMeasurer, which is available for tree structures in https://diogofcunha.github.io/react-virtualized-tree/#/examples/node-measure.

The resize function sounds like it should help with this, if I had a way to measure the height of the element directly. react-window seems to be working on just-in-time measured content in bvaughn/react-window#6. But it's not very clear to me how could it be applied to the vtree nodes.

Is there a good way to determine the node height and pass it to the resize function, similar to CellMeasurer?

treeWalker is walking all nodes

I'm using 3.0.0-beta.1. It seems that my provided treeWalker generator is being walked to visit every node in the tree, even though most of the nodes are not visible (collapsed).

Is this how it is intended to function, or am I missing something? My use case involves a tree that is very large and very deep, and computing the nodes is somewhat expensive. Users only visit a small subset of the nodes at a time, so my hope was that the treeWalker would only visit the visible nodes and save a bunch of unnecessary computation.

As it is, there is a very noticeable (few seconds) delay on expanding/collapsing nodes (because the walks the tree again).

Filtering of nodes

Hi @Lodin, thank you very much for this awesome project. I'm curious and seeking advice on implementing the filtering and totaling the children of a given node.

If I understand correctly, we need to implement it externally and pass the filtered tree using the treeWalker. I'm wondering is there any way we can leverage the treeWalker to filter and find the total nodes below the given node.

Example tree for filtering nodes

Node 1 (Rob Total = 2)
 |- Node 1.1 (Rob Total = 2)
      |- Node 1.1.1
      |- Rob 1.1.2
      |- Rob 1.1.3
 |- Node 1.2
Node 2 (Rob Total = 1)
 |- Node 2.1 (Rob Total = 1)
     |- Rob 2.1.1

If we find a match of Node 1.1.1, we need to add the entire parent chain in the result tree and update the children's total accordingly. I have implemented it already outside of treeWalker, however, just curious to know your thoughts.

Confused about Migrating 2.x.x -> 3.x.x

Hi,

On the npm site - https://www.npmjs.com/package/react-vtree- there is "2.0.4" version visible as the latest one.

I've just installed react-vtree like:
npm install react-vtree
and in my package.json I can see:
"react-vtree": "^2.0.4",

On the readme page of the react-vtree I can see a note

NOTE: This is the documentation for version 3.x.x.

and a migration section - https://github.com/Lodin/react-vtree#migrating-2xx---3xx-

I tried to adopt my code regardles to the documentation from readme file but I have many issues e.g. isOpen and setOpen are undefined. Then I tried to access "toggle" and this function is available. So it seems that master branch points to version 2 but there is no tag/branch for version 3 in this repo..?

Can you please tell me how can I download version 3 of react-vtree ?

UPDATE:
Cloning the repo from master branch, building manually and copying to node_modules delivers version 3 but I'd like to install it from npm, so help still needed here.

[QUESTION] How can I render children nodes inside their parents?

Hi, I have been trying to make a virtualized material-ui TreeView. I am trying to use react-vtree but in order to make it work I need to render children nodes inside their parent. E.g:

<TreeItem>
  <TreeItem>
    <TreeItem>
    </TreeItem>
  </TreeItem>
  <TreeItem>
    <TreeItem>
    </TreeItem>
  </TreeItem>
</TreeItem>

Is there anyway I can achieve this? As far as I can see, your examples render all the elements in the same level (like a list).

Thanks

toggle issue in 1.0.1

I think there is a problem with toggle in 1.0.1.

Having upgraded from 1.0.0, I am receiving the following error on calling toggle for a given node (both in my app and when using the repo's storybook):

Uncaught (in promise) TypeError: Cannot read property 'isOpen' of undefined
    at toggle (FixedSizeTree.tsx:76)

Thanks for all the work on the project.

Recompute tree when treeWalker changes?

Spent a day figuring out why the tree wouldn't update when I change tree data. It turned out the FixedSizeList just doesn't update list if new treeWalker is passed.

Is this a bug or a feature?

My use case is that I have a list with checkboxes which modifies the tree data that I need to render. So for every change in data I create a new treeWalker.

Add `listRef` prop

It may be necessary to get the reference to the FixedSizeList or VariableSizeList that lays under the Tree component. For this, the listRef property should be added.

const Tree = () => {
  const ref = useRef();

  return (
    <FixedSizeTree
      listRef={ref} // <--
      treeWalker={treeWalker}
      itemSize={itemSize}
      height={height}
      width="100%"
    >
      {Node}
    </FixedSizeTree>
  );
}

Can we get More Documentation and a working example of how to use scrollToItem

I am finding it difficult to get scrollToItem to work, I have the reference to the tree and call ref.current.scrollToItem(row) in but it never seems to scrollToItem (even when all parent nodes are already open)

Also it is unclear to me how we can know the index for an item without all its parent nodes already opened as when subtree is collapsed it changes all the index's
I would expect to be able to have an id for a node and if I scroll to said id it would open any parent nodes currently closed and scroll to said item

Maybe my expectation/understanding is wrong?

Drag and Drop

Hi! I like your work a lot and let me preface this by saying this is not a request for the feature to be implemented into the library. My question is - how much time do you think it would take to integrate either a react-specific DND (react-dnd) or regular HTML5 DND with this library?

Bump React version to 17

Since react-window supports React 17, I think it is reasonable for this package to support it too. My specific use-case is including in a NextJS v11 project. Thanks you.

Use tree node id as a key for Row component

To reduce amount of re-painting, the Row component can use the node's id as the key through the getItemKey() function provided by react-window. That should also improve the performance when tree opens/closes.

Breaking change

This issue, however, requires removing the symbol support for the node's id because React is able to use only string keys.

Stop building list and wait for next page of children data to be loaded

Hi there,
Thank you for the amazing library. I have an interesting issue I am trying to sort out with vtree.

I am trying to use vtree with an API that uses cursor based pagination. For example, at the root I would load all root items(files/folders). The API call would return the first 20 nodes as well as a hasNextPage variable.

If there is a next page I am trying to insert a placeholder that will then load the additional items when it is about to be loaded into the view. This seems like it will work fine.

The thing I can't figure out is how to do this inside the children of an item. If I load the first roots nodes children and it has 50 children but only the first 20 children of a folder are loaded and you are scrolling through you should not be able to see root nodes until all 50 of the children have been loaded and scrolled past. Is there a way to stop building the rest off the tree if and instead load the additional children before continuing to scroll the other root items? I tried adding an early return in the treewalker generator function, but this doesn't seem to work.

It is also very possible I am going about this all wrong. I'll try to make a diagram to make this explanation a bit clearer.

stable version

This looks exactly like what I need!
What are some of the tasks needed for a stable release of this?

Async data tree bug

Hello,

I discovered a bug(?) in v3 async tree. This bug can be replicated in the Async data tree demo.

First if a user opens e.g. three levels of child nodes under a parent, then closes the parent and opens another parent, some of the previous parents nodes pop open as well.

See gif =>

Tree - Async data ⋅ Storybook - Google Chrome 2021-01-18 12-37-45

Setting component state onClick causes the tree to lose focus position during keyboard navigation and go to top of the rendered tree

This is during keyboard navigation: in my use case, I need to keep track of the "selected" node in state so I can apply specific styles to it - when a node is clicked setState is called.

In this simplified example, I have a state variable called 'test' and am calling setTest in the onClick of the node. If you press 'tab' down the nodes and then press 'enter' and 'tab' again, you'll see that the focus is set to the top of the tree instead of staying in place.
Example: https://codesandbox.io/s/react-vtree-forked-n8xl7l?file=/src/App.tsx

Using react-dev-tools I see that each Row has a unique key set as expected, so I'm not sure what the issue is, if anyone has any ideas please let me know!

Cannot preserve openness while adding / removing nodes from tree?

From the new 3.x API, it looks like if the nodes in the tree changes (by adding / removing nodes), a new treeWalker is required, which will reset all state, including the existing openness of individual nodes. Is that right? Is there a way to preserve openness while mutating the tree?

children type NodeComponentType not exposed for consumption

The children prop is typed as NodeComponentProps on TreeProps, however the type is not exported. Only TreeWalker and TreeWalkerValue types get exported from ./Tree as you can see here

In order to properly type my custom tree node component this type should be exposed to the consumer of the library

Cannot toggle OpennessState of node that defaulted to closed

I am attempting to control the openness state of some nodes under certain conditions in my project. But I have found this is doesn't seem to work unless all the nodes have isOpenByDefault: true

Here is a minimal example I have reproduced that is similar to what I am hoping to achieve.

https://codesandbox.io/s/admiring-tree-k0x04?file=/src/Tree.tsx

You can see I want to default the root node to be open. and default its direct children to closed.

Under some circumstance (in this example it's just after the tree has initialized for simplicity), I want to open a node. The Button for the node changed to -, as if it thinks its open, but the children do not render.

There are also some errors in the console that nodes cannot be found. No record with id child-2 found.

I also notice if I toggle any of the nodes, the whole tree then updates correctly. In this example, clicking "Child 4", expands Child 4, but also the node I toggled with opennessState then renders its children correctly.

Is this a known constraint of the openness state control? or am I trying to approach this in the wrong way?

Collapse specific row

Is there a function for collapsing specific row by accepting row id as an argument? Something like scrollToItem() function.

Changing async tree shape re-opens nested items

Example scenario: I have a tree with the following structure:

- root
  - a
    - a1
      - a11
  + b

where on expanding a node, I load more data from an API, update my root node object in a parent component, and trigger a rebuild by changing treeWalker.

If I have expanded nodes as in the example above and then collapse the a node without collapsing its children manually:

- root
  + a
  + b

and then load more data (updating the root node object) by expanding b, the tree will look like the following:

- root
  + a
      - a11
  - b
    - b1

Where the unexpected behavior is that we will see the node a11 show up, even though its grandparent a is collapsed. This doesn't happen if the root node never changes (if we have the tree shape from a separate api call, and load each node based on their id in a node component).

Are we required to know an async tree's shape beforehand (like in the AsyncTree example), or should we be able to edit the tree's shape based on lazily loaded data?

Deleting a node from tree

I am adding dynamic nodes by modifying the data structure( which is a prop) and adding children at appropriate levels. I enabled async so the tree open state remains fine. Just the children are added dynamically. So that works fine.
But when I do the same for delete the node is deleted from the data structure which is passed a prop but the tree is not re-rendered with that removed node.

Returning undefined from treeWalker throws error

Hey there, looks like currently Tree.tsx assumes that treeWalker will always yield valid node data and that it will never be undefined, even though that is a valid return value for yield expressions (there is even a non-null assertion on the yielded value).

I'd like to propose that this behaviour be changed so that undefined values are handled gracefully as it would allow returning early in the case of not having nodes to render yet, etc, whereas right now an Cannot read property 'data' of undefined error is thrown.

@Lodin I'd be happy to PR a fix here but I'm not quite sure what the desired behaviour would be in this case, should I just return early with null inside of generateNewTree?

Anyways, thanks for an otherwise awesome library!

Bug while expanding nodes with more than 32766 children - v3

Frist of all, thanks for the amazing work put into the lib. It's very awesome.

Conditions for reproducing:

  • You need to have a node (lets call A) with 32767 children or more (do not know if they need to be direct children, I think not)
  • And you need to have one of more nodes after node A.
  • All my testing was done in latest chrome, using windows 10 and MacOs

I prepared a reproduction of the bug here: https://github.com/sirgallifrey/react-vtree/tree/openning-lots-of-nodes

How to reproduce:

  • Checkout my fork on the branch called: openning-lots-of-nodes
  • run storybook:dev script
  • Go to story called Big List Issue
  • Scroll all the way to the bottom and observe that everything renders properly so far.
  • Scroll back up and collapse the node called Root.2
  • Observe that everything still works as expected.
  • Expand the node called Root.2 and scroll all the way to the bottom
  • Observe that some nodes appear missing, some may be duplicated.
  • From now on, collapsing and expanding Root.2 will not fix the issue.
  • Collapsing and then expanding the node Root will fix the issue end render everything properly, but issue can comeback by expanding Root.2

There is one extra glitch that I'm seeing in this reproduction that I didn't saw when I first used the lib: If you keep collapsing and expanding Root.2 you will end up with a ghost floating element that will not go away even by collapsing and expanding the parent. But the following workaround prevents that

Workaround:

Since opening the root node fixes the issue, it is possible to use recomputeTree instead of setOpen and always open the root node with every instruction, an example of the workaround can be found on my fork, in the story named: Big List Issue workaround

I didn't had time yet to try find the bug on the code, but since the issue happens at such specific number of itens it's probably happening inside the updateExistingTree https://github.com/Lodin/react-vtree/blob/master/src/Tree.tsx#L402

Pictures / videos:

Screen.Recording.2021-01-29.at.17.42.01.mov

Screen Shot 2021-01-29 at 17 36 53

Implement setOpen function for the Node component

To achieve more fine-grained control over the Node component, it looks like a good idea to implement the setOpen function along with the already existing toggle. It should have the following interface:

function setOpen(state: boolean): void;

The example of usage could be the following:

const Node = ({
  data: {isLeaf, name, nestingLevel},
  isOpen,
  style,
  setOpen,
}) => {
  <div
    style={{
      ...style,
      marginLeft: nestingLevel * 30 + (isLeaf ? 48 : 0),
    }}
  >
    {!isLeaf && (
      <div>
        // emulating the `toggle` function
        <button type="button" onClick={() => setOpen(!isOpen)>
          {isOpen ? '-' : '+'}
        </button>
      </div>
    )}
    <div>{name}</div>
  </div>
}

The expanded children nodes are expanded even when we collapsed at the parent level.

Hi,
I encountered one scenario -

  • Let's say we have four nested nodes. Expand the nodes one by one till the last nested children.

  • Now, collapse at the most top-level parent, it would get collapse. But when expanded again, all nested nodes were found in the same open state.

Once collapsed at the parent level, all the sub-nodes should also collapse every time.

isOpenByDefault: false/true didn't help on this as it just helps to maintain an initial state of all nodes (whether to keep all nodes as in expanded or collapse state)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.