3.0.0-beta.0
Pre-releaseThis release contains the significant number of changes of the library. The main theme of the release is to increase the speed of rendering giant trees (trees with ~1 million nodes) which required the re-writing of the tree building and tree updating algorithms.
Breaking changes
treeWalker
The treeWalker
function has the completely different shape now.
Old treeWalker
worked for both initial tree building and changing node openness state:
Old treeWalker
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.
New treeWalker
// 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,
);
}
}
}
recomputeTree
The recomputeTree
method has been completely re-worked. Now it is way more powerful than its predecessor, and the main role in this change is played by subtreeCallback
function that applies to all descendants of the selected node. With it, you can do a lot of things including emulation of the 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,
});
toggle
-> setOpen
The toggle
function sent to the Node
component is replaced with setOpen
function. setOpen
function has the following signature and allows specifying the node openness state directly without relying on the internal state.
function setOpen(state: boolean): void;
toggle
const Node = ({
data: {isLeaf, name, nestingLevel},
isOpen,
style,
toggle,
}) => {
<div
style={{
...style,
marginLeft: nestingLevel * 30 + (isLeaf ? 48 : 0),
}}
>
{!isLeaf && (
<div>
// Here the `toggle` function is used
<button type="button" onClick={toggle>
{isOpen ? '-' : '+'}
</button>
</div>
)}
<div>{name}</div>
</div>
}
setOpen
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>
}
Node id
can be only string
now
Since we have a tree that reflected is reflected as a virtual list, it is a good idea to use node id
s as React keys to speed up the React re-rendering on toggling the nodes. It would allow re-rendering only the required elements instead of re-rendering whole tree. However, it also forbids using symbol
as id
because React do not support symbols as keys.
Features
New props
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.
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.
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.
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.
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.
Migrating 2.x.x
-> 3.x.x
Also described in README.
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.