Skip to content

Re-implement tree to work with giant structures #40

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 35 commits into from
Nov 4, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
80635a3
fix: improve speed of node opening/closing for a large amount of nodes
Lodin Sep 10, 2020
3873613
refactor: split updating algorithm to building and updating ones
Lodin Sep 14, 2020
179eb4d
refactor: use point replacements for order array instead of its full …
Lodin Sep 14, 2020
747f2ff
refactor: improve record data shape and openness state options
Lodin Sep 23, 2020
91e3cb5
refactor: fix some issues and split computeTree function to separate …
Lodin Sep 26, 2020
1b5d9b0
refactor: replace useDefault* with subtreeCallback and improve typings
Lodin Sep 26, 2020
fffbf2d
test: update tests
Lodin Sep 26, 2020
c00c551
build: update config
Lodin Sep 26, 2020
8490495
refactor: use WeakMap for storing temporary node metadata.
Lodin Sep 27, 2020
f96bdf9
refactor: improve tree building performance
Lodin Sep 27, 2020
ec58abf
refactor: improve tree building performance [2]
Lodin Sep 27, 2020
13a69ce
style: improve naming
Lodin Sep 28, 2020
15c4e3a
docs: update documentation
Lodin Sep 28, 2020
49c1af6
feat: allow multiple tree roots
Lodin Oct 12, 2020
5540854
build(eslint): disable "@typescript-eslint/prefer-for-of" rule
Lodin Oct 13, 2020
b17d1a3
fix: avoid stack overflow exception for tree update
Lodin Oct 13, 2020
9e9b4ba
feat: add feature to unfreeze UI while tree is building
Lodin Oct 16, 2020
2473059
refactor: rename buildingNode -> placeholder
Lodin Oct 17, 2020
a6eb531
Merge branch 'master' into fix/performance-on-big-data
Lodin Oct 18, 2020
fad9bb0
test: add tests for requestIdleCallback feature
Lodin Oct 18, 2020
ce632c9
Merge branch 'master' into fix/performance-on-big-data
Lodin Oct 18, 2020
b3d505c
feat: allow preserving the state between the tree buildings
Lodin Oct 19, 2020
850931d
refactor: fix tree node closing bug & add some minor code improvements
Lodin Oct 26, 2020
8fe7be5
feat: add more stories for useful cases
Lodin Oct 26, 2020
85cb609
refactor: rename preservePreviousState -> async
Lodin Nov 4, 2020
e8a8266
fix: avoid undefined behavior on scheduler finalization
Lodin Nov 4, 2020
c02e23d
docs: add description for async prop
Lodin Nov 4, 2020
52742ae
docs: fix placeholder display in BigData story
Lodin Nov 4, 2020
606125a
test: add tests for null placeholder
Lodin Nov 4, 2020
6d68b31
docs: add description for null placeholder
Lodin Nov 4, 2020
d951f2d
docs: add migration guide
Lodin Nov 4, 2020
1bfd51e
docs: add link to the v2
Lodin Nov 4, 2020
186ad4d
docs: improve v2 version linking
Lodin Nov 4, 2020
ba5f5ab
docs: improve version naming
Lodin Nov 4, 2020
0486535
docs: small improvements
Lodin Nov 4, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
},
"rules": {
"@typescript-eslint/promise-function-async": "off",
"@typescript-eslint/prefer-for-of": "off",
"guard-for-in": "off"
}
}
583 changes: 435 additions & 148 deletions README.md

Large diffs are not rendered by default.

212 changes: 212 additions & 0 deletions __stories__/AsyncData.story.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
/* eslint-disable max-depth */
import {boolean, number, withKnobs} from '@storybook/addon-knobs';
import {storiesOf} from '@storybook/react';
import React, {FC, useCallback, useMemo, useRef, useState} from 'react';
import AutoSizer from 'react-virtualized-auto-sizer';
import {
FixedSizeNodeData,
FixedSizeNodePublicState,
FixedSizeTree,
TreeWalker,
TreeWalkerValue,
} from '../src';
import {NodeComponentProps} from '../src/Tree';
import {AsyncTaskScheduler} from './utils';

document.body.style.margin = '0';
document.body.style.display = 'flex';
document.body.style.minHeight = '100vh';

const root = document.getElementById('root')!;
root.style.margin = '10px 0 0 10px';
root.style.flex = '1';

type TreeNode = Readonly<{
children: TreeNode[];
downloaded: boolean;
id: number;
name: string;
}>;

type TreeData = FixedSizeNodeData &
Readonly<{
downloaded: boolean;
download: () => Promise<void>;
isLeaf: boolean;
name: string;
nestingLevel: number;
}>;

let nodeId = 0;

const createNode = (
downloadedIds: readonly number[],
depth: number = 0,
): TreeNode => {
const id = nodeId;
const node: TreeNode = {
children: [],
downloaded: downloadedIds.includes(id),
id,
name: `test-${nodeId}`,
};

nodeId += 1;

if (depth === 5) {
return node;
}

for (let i = 0; i < 10; i++) {
node.children.push(createNode(downloadedIds, depth + 1));
}

return node;
};

const defaultTextStyle = {marginLeft: 10};
const defaultButtonStyle = {fontFamily: 'Courier New'};

type NodeMeta = Readonly<{
nestingLevel: number;
node: TreeNode;
}>;

const getNodeData = (
node: TreeNode,
nestingLevel: number,
download: () => Promise<void>,
): TreeWalkerValue<TreeData, NodeMeta> => ({
data: {
download,
downloaded: node.downloaded,
id: node.id.toString(),
isLeaf: node.children.length === 0,
isOpenByDefault: false,
name: node.name,
nestingLevel,
},
nestingLevel,
node,
});

const Node: FC<NodeComponentProps<
TreeData,
FixedSizeNodePublicState<TreeData>
>> = ({
data: {download, downloaded, isLeaf, name, nestingLevel},
isOpen,
style,
toggle,
}) => {
const [isLoading, setLoading] = useState(false);

return (
<div
style={{
...style,
alignItems: 'center',
display: 'flex',
marginLeft: nestingLevel * 30 + (isLeaf ? 48 : 0),
}}
>
{!isLeaf && (
<div>
<button
type="button"
onClick={async () => {
if (!downloaded) {
setLoading(true);
await download();
await toggle();
setLoading(false);
} else {
await toggle();
}
}}
style={defaultButtonStyle}
>
{isLoading ? '⌛' : isOpen ? '-' : '+'}
</button>
</div>
)}
<div style={defaultTextStyle}>{name}</div>
</div>
);
};

type TreePresenterProps = Readonly<{
disableAsync: boolean;
itemSize: number;
}>;

const TreePresenter: FC<TreePresenterProps> = ({disableAsync, itemSize}) => {
const [downloadedIds, setDownloadedIds] = useState<readonly number[]>([]);
const scheduler = useRef<AsyncTaskScheduler<number>>(
new AsyncTaskScheduler((ids) => {
setDownloadedIds(ids);
}),
);
const rootNode = useMemo(() => {
nodeId = 0;

return createNode(downloadedIds);
}, [downloadedIds]);

const createDownloader = (node: TreeNode) => (): Promise<void> =>
new Promise((resolve) => {
const timeoutId = setTimeout(() => {
scheduler.current.finalize();
}, 2000);

scheduler.current.add(node.id, resolve, () => clearTimeout(timeoutId));
});

const treeWalker = useCallback(
function* treeWalker(): ReturnType<TreeWalker<TreeData, NodeMeta>> {
yield getNodeData(rootNode, 0, createDownloader(rootNode));

// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
while (true) {
const parentMeta = yield;

if (parentMeta.data.downloaded) {
// eslint-disable-next-line @typescript-eslint/prefer-for-of
for (let i = 0; i < parentMeta.node.children.length; i++) {
yield getNodeData(
parentMeta.node.children[i],
parentMeta.nestingLevel + 1,
createDownloader(parentMeta.node.children[i]),
);
}
}
}
},
[rootNode],
);

return (
<AutoSizer disableWidth>
{({height}) => (
<FixedSizeTree
treeWalker={treeWalker}
itemSize={itemSize}
height={height}
async={!disableAsync}
width="100%"
>
{Node}
</FixedSizeTree>
)}
</AutoSizer>
);
};

storiesOf('Tree', module)
.addDecorator(withKnobs)
.add('Async data', () => (
<TreePresenter
disableAsync={boolean('Disable async', false)}
itemSize={number('Row height', 30)}
/>
));
Loading