react-vtree
Version:
React component for efficiently rendering large tree structures
329 lines (248 loc) • 13 kB
Markdown
# react-vtree
[](https://www.npmjs.com/package/react-vtree)
[](./LICENSE)
[](https://github.com/Lodin/react-vtree/actions)
[](https://sonarcloud.io/dashboard?id=Lodin_react-vtree)
[](https://sonarcloud.io/dashboard?id=Lodin_react-vtree)
[](https://sonarcloud.io/dashboard?id=Lodin_react-vtree)
This package provides a lightweight and flexible solution for rendering large tree structures. It is built on top of the [react-window](https://github.com/bvaughn/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](https://github.com/bvaughn/react-virtualized), take a look at [react-virtualized-tree](https://github.com/diogofcunha/react-virtualized-tree).
## Installation
```bash
# 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:
- [Source code](./__stories__/FixedSizeTree.story.tsx)
- [Demo](https://lodin.github.io/react-vtree/index.html?path=/story/tree--fixedsizetree)
```javascript
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) {
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();
// 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.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.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 = ({data: {isLeaf, name}, isOpen, style, toggle}) => (
<div style={style}>
{!isLeaf && (
<button type="button" onClick={toggle}>
{isOpen ? '-' : '+'}
</button>
)}
<div>{name}</div>
</div>
);
const Example = () => (
<Tree treeWalker={treeWalker} itemSize={30} height={150} width={300}>
{Node}
</Tree>
);
```
#### Props
The component receives all the props of the `FixedSizeList` component except for the `itemCount`. Additional properties are the following:
##### `children`
The `Node` component that is responsible for rendering each node. It receives all the properties [`Row`](https://react-window.now.sh/#/api/FixedSizeList) recieves except for the `index` prop plus the following properties:
- `data: object` - a data object yielded by the `treeWalker` function.
- `isOpen: boolean` - a current openness status of the node.
- `toggle(): function` - a function to change the openness state of the node. It receives no arguments and can be provided directly as an `onClick` handler.
- `treeData: any` - any data provided via the `itemData` property of the `FixedSizeTree` component.
##### `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 to an existing one by wrapping the default `Row` into a custom component.
##### `* treeWalker(refresh: boolean)`
An iterator function that walks around the tree and yields each node one by one flattening them to an array that can be easily displayed by `FixedSizeList` component.
The function receives `refresh` parameter. If it is `true`, the component requests the full node update and expects the complete data object yielded. If it is `false`, the component awaits only the node id to update the order of displayed nodes.
The data object should contain the following required properties:
- `id` - a unique identifier of the node.
- `isOpenByDefault` - a default openness state of the node.
You can add any other property you need. This object will be sent directly to the `Node` component.
Yielding the object gets the current openness state of the node. Basing on it, you should decide if the node's children are going to be rendered.
#### Methods
The component provides all the methods `FixedSizeList` provides with the following changes:
##### `scrollToItem(id: string | symbol, align?: Align): void`
The `scrollToItem` method receives node `id` instead of `index`.
##### `async recomputeTree(options): void`
This method runs the `treeWalker` function again and, basing on the received options, updates either nodes or their order.
It receives options object with the following parameters:
- `opennessState: Record<string, boolean>` - nodes whose IDs are specified as keys of this object will be opened or closed according to boolean values. If the value is `true`, node will be opened; otherwise, it will be closed. This object can be used for changing nodes' openness programmatically without re-creating the `treeWalker` generator.
**NOTE**: If you specify both `useDefaultOpenness` and `opennessState`, `opennessState` will be overridden by `useDefaultOpenness` results.
- `refreshNodes: boolean` - if this parameter is `true`, `treeWalker` will receive `refresh` option, and the component will expect the data object yielded. If this parameter is either `false` or not provided, the component will expect string id.
- `useDefaultOpenness: boolean` - if this parameter is `true`, openness state of all nodes will be reset to `isOpenByDefault`. Nodes updated during the tree walking will use the new `isOpenByDefault` value.
#### Types
- `FixedSizeNodeData` - object the `treeWalker` generator function yields for each new node. By default, it contains only `id` and `isOpenByDefault` fields, but you can add any number of additional fields; they will be sent directly to the Node component. To describe that data, you have to create a new type that extends the `FixedSizeNodeData` type.
- `FixedSizeNodeComponentProps<T extends FixedSizeNodeData>` - props that `Node` component receives. They are described in the Props [children](#children) section.
- `FixedSizeTreeProps<T extends FixedSizeNodeData>` - props that `FixedSizeTree` component receives. Described in the [Props](#props) section.
- `FixedSizeTreeState<T extends FixedSizeNodeData>` - state that `FixedSizeTree` component has.
### `VariableSizeTree`
#### Example
You can also take a look at the very similar example at the Storybook:
- [Source code](./__stories__/VariableSizeTree.story.tsx)
- [Demo](https://lodin.github.io/react-vtree/index.html?path=/story/tree--variablesizetree)
```javascript
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',
},
],
};
function* treeWalker(refresh) {
const stack = [];
stack.push({
nestingLevel: 0,
node: tree,
});
while (stack.length !== 0) {
const {
node: {children = [], id, name},
nestingLevel,
} = stack.pop();
const isOpened = yield refresh
? {
// The only difference VariableSizeTree `treeWalker` has comparing to
// the FixedSizeTree is the `defaultHeight` property in the data
// object.
defaultHeight: 30,
id,
isLeaf: children.length === 0,
isOpenByDefault: true,
name,
nestingLevel,
}
: id;
if (children.length !== 0 && isOpened) {
for (let i = children.length - 1; i >= 0; i--) {
stack.push({
nestingLevel: nestingLevel + 1,
node: children[i],
});
}
}
}
}
// Node component receives current node height as a prop
const Node = ({data: {isLeaf, name}, height, isOpen, style, toggle}) => (
<div style={style}>
{!isLeaf && (
<button type="button" onClick={toggle}>
{isOpen ? '-' : '+'}
</button>
)}
<div>{name}</div>
</div>
);
const Example = () => (
<Tree treeWalker={treeWalker} height={150} width={300}>
{Node}
</Tree>
);
```
#### Props
The component receives all the props of the `VariableSizeList` component except for the `itemCount` and `itemSize`. `itemSize` is still available but not required, and should be used only if the default behavior is not enough. Additional properties are the following:
##### `children`
The `Node` component. It is the same as the [`FixedSizeTree`](#fixedsizetree)'s one but receives two additional properties:
- `height: number` - a current height of the node.
- `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 to the [`resetAfterIndex`](https://react-window.now.sh/#/api/VariableSizeList) method.
##### `rowComponent: component`
This property receives a custom `Row` component for the `VariableSizeList` that will override the default one. It can be used for adding new functionality to an existing one by wrapping the default `Row` into a custom component.
##### `* treeWalker(refresh: boolean)`
An iterator function that walks over the tree. It behaves the same as `FixedSizeTree`'s `treeWalker`, but there one additional required property for the data object:
- `defaultHeight: number` - the default height of the node.
#### Methods
The component provides all the methods `VariableSizeList` provides with the following changes:
##### `scrollToItem(id: string | symbol, align?: Align): void`
The `scrollToItem` method 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(options): void`
This method works exactly the same as the `FixedSizeTree`'s one, but receives one additional option:
- `useDefaultHeight: boolean` - if this parameter is `true`, the height of all nodes will be reset to `defaultHeight`. Nodes updated during the tree walking will use the new `defaultHeight` value.
#### Types
All types in this section are the extended variants of [`FixedSizeTree` types](#types).
- `VariableSizeNodeData`
- `VariableSizeNodeComponentProps<T extends VariableSizeNodeData>`.
- `VariableSizeTreeProps<T extends VariableSizeNodeData>`.
- `VariableSizeTreeState<T extends VariableSizeNodeData>`.