Skip to content

Commit 2de16ae

Browse files
authored
Merge pull request #40 from Lodin/fix/performance-on-big-data
Re-implement tree to work with giant structures
2 parents bf812eb + 0486535 commit 2de16ae

21 files changed

+2814
-1572
lines changed

.eslintrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
},
1414
"rules": {
1515
"@typescript-eslint/promise-function-async": "off",
16+
"@typescript-eslint/prefer-for-of": "off",
1617
"guard-for-in": "off"
1718
}
1819
}

README.md

Lines changed: 435 additions & 148 deletions
Large diffs are not rendered by default.

__stories__/AsyncData.story.tsx

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
/* eslint-disable max-depth */
2+
import {boolean, number, withKnobs} from '@storybook/addon-knobs';
3+
import {storiesOf} from '@storybook/react';
4+
import React, {FC, useCallback, useMemo, useRef, useState} from 'react';
5+
import AutoSizer from 'react-virtualized-auto-sizer';
6+
import {
7+
FixedSizeNodeData,
8+
FixedSizeNodePublicState,
9+
FixedSizeTree,
10+
TreeWalker,
11+
TreeWalkerValue,
12+
} from '../src';
13+
import {NodeComponentProps} from '../src/Tree';
14+
import {AsyncTaskScheduler} from './utils';
15+
16+
document.body.style.margin = '0';
17+
document.body.style.display = 'flex';
18+
document.body.style.minHeight = '100vh';
19+
20+
const root = document.getElementById('root')!;
21+
root.style.margin = '10px 0 0 10px';
22+
root.style.flex = '1';
23+
24+
type TreeNode = Readonly<{
25+
children: TreeNode[];
26+
downloaded: boolean;
27+
id: number;
28+
name: string;
29+
}>;
30+
31+
type TreeData = FixedSizeNodeData &
32+
Readonly<{
33+
downloaded: boolean;
34+
download: () => Promise<void>;
35+
isLeaf: boolean;
36+
name: string;
37+
nestingLevel: number;
38+
}>;
39+
40+
let nodeId = 0;
41+
42+
const createNode = (
43+
downloadedIds: readonly number[],
44+
depth: number = 0,
45+
): TreeNode => {
46+
const id = nodeId;
47+
const node: TreeNode = {
48+
children: [],
49+
downloaded: downloadedIds.includes(id),
50+
id,
51+
name: `test-${nodeId}`,
52+
};
53+
54+
nodeId += 1;
55+
56+
if (depth === 5) {
57+
return node;
58+
}
59+
60+
for (let i = 0; i < 10; i++) {
61+
node.children.push(createNode(downloadedIds, depth + 1));
62+
}
63+
64+
return node;
65+
};
66+
67+
const defaultTextStyle = {marginLeft: 10};
68+
const defaultButtonStyle = {fontFamily: 'Courier New'};
69+
70+
type NodeMeta = Readonly<{
71+
nestingLevel: number;
72+
node: TreeNode;
73+
}>;
74+
75+
const getNodeData = (
76+
node: TreeNode,
77+
nestingLevel: number,
78+
download: () => Promise<void>,
79+
): TreeWalkerValue<TreeData, NodeMeta> => ({
80+
data: {
81+
download,
82+
downloaded: node.downloaded,
83+
id: node.id.toString(),
84+
isLeaf: node.children.length === 0,
85+
isOpenByDefault: false,
86+
name: node.name,
87+
nestingLevel,
88+
},
89+
nestingLevel,
90+
node,
91+
});
92+
93+
const Node: FC<NodeComponentProps<
94+
TreeData,
95+
FixedSizeNodePublicState<TreeData>
96+
>> = ({
97+
data: {download, downloaded, isLeaf, name, nestingLevel},
98+
isOpen,
99+
style,
100+
toggle,
101+
}) => {
102+
const [isLoading, setLoading] = useState(false);
103+
104+
return (
105+
<div
106+
style={{
107+
...style,
108+
alignItems: 'center',
109+
display: 'flex',
110+
marginLeft: nestingLevel * 30 + (isLeaf ? 48 : 0),
111+
}}
112+
>
113+
{!isLeaf && (
114+
<div>
115+
<button
116+
type="button"
117+
onClick={async () => {
118+
if (!downloaded) {
119+
setLoading(true);
120+
await download();
121+
await toggle();
122+
setLoading(false);
123+
} else {
124+
await toggle();
125+
}
126+
}}
127+
style={defaultButtonStyle}
128+
>
129+
{isLoading ? '⌛' : isOpen ? '-' : '+'}
130+
</button>
131+
</div>
132+
)}
133+
<div style={defaultTextStyle}>{name}</div>
134+
</div>
135+
);
136+
};
137+
138+
type TreePresenterProps = Readonly<{
139+
disableAsync: boolean;
140+
itemSize: number;
141+
}>;
142+
143+
const TreePresenter: FC<TreePresenterProps> = ({disableAsync, itemSize}) => {
144+
const [downloadedIds, setDownloadedIds] = useState<readonly number[]>([]);
145+
const scheduler = useRef<AsyncTaskScheduler<number>>(
146+
new AsyncTaskScheduler((ids) => {
147+
setDownloadedIds(ids);
148+
}),
149+
);
150+
const rootNode = useMemo(() => {
151+
nodeId = 0;
152+
153+
return createNode(downloadedIds);
154+
}, [downloadedIds]);
155+
156+
const createDownloader = (node: TreeNode) => (): Promise<void> =>
157+
new Promise((resolve) => {
158+
const timeoutId = setTimeout(() => {
159+
scheduler.current.finalize();
160+
}, 2000);
161+
162+
scheduler.current.add(node.id, resolve, () => clearTimeout(timeoutId));
163+
});
164+
165+
const treeWalker = useCallback(
166+
function* treeWalker(): ReturnType<TreeWalker<TreeData, NodeMeta>> {
167+
yield getNodeData(rootNode, 0, createDownloader(rootNode));
168+
169+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
170+
while (true) {
171+
const parentMeta = yield;
172+
173+
if (parentMeta.data.downloaded) {
174+
// eslint-disable-next-line @typescript-eslint/prefer-for-of
175+
for (let i = 0; i < parentMeta.node.children.length; i++) {
176+
yield getNodeData(
177+
parentMeta.node.children[i],
178+
parentMeta.nestingLevel + 1,
179+
createDownloader(parentMeta.node.children[i]),
180+
);
181+
}
182+
}
183+
}
184+
},
185+
[rootNode],
186+
);
187+
188+
return (
189+
<AutoSizer disableWidth>
190+
{({height}) => (
191+
<FixedSizeTree
192+
treeWalker={treeWalker}
193+
itemSize={itemSize}
194+
height={height}
195+
async={!disableAsync}
196+
width="100%"
197+
>
198+
{Node}
199+
</FixedSizeTree>
200+
)}
201+
</AutoSizer>
202+
);
203+
};
204+
205+
storiesOf('Tree', module)
206+
.addDecorator(withKnobs)
207+
.add('Async data', () => (
208+
<TreePresenter
209+
disableAsync={boolean('Disable async', false)}
210+
itemSize={number('Row height', 30)}
211+
/>
212+
));

0 commit comments

Comments
 (0)