Skip to content

Commit d29cb9e

Browse files
authored
Merge pull request #57 from sirgallifrey/fix/33k-children-issue-52
Fix expanding nodes with more than 32768 children
2 parents 08fe2d9 + 1f40879 commit d29cb9e

File tree

3 files changed

+143
-19
lines changed

3 files changed

+143
-19
lines changed

__tests__/FixedSizeTree.spec.tsx

Lines changed: 96 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {mount, ReactWrapper} from 'enzyme';
22
import React, {createRef, FC} from 'react';
3-
import {FixedSizeList} from 'react-window';
3+
import {FixedSizeList, FixedSizeListProps} from 'react-window';
44
import {
55
FixedSizeNodeData,
66
FixedSizeNodePublicState,
@@ -11,12 +11,13 @@ import {
1111
TreeWalker,
1212
TreeWalkerValue,
1313
} from '../src';
14-
import {NodeComponentProps} from '../src/Tree';
14+
import {NodeComponentProps, NodePublicState} from '../src/Tree';
1515
import {
1616
defaultTree,
1717
extractReceivedRecords,
1818
mockRequestIdleCallback,
1919
sleep,
20+
treeWithLargeNode,
2021
} from './utils/misc';
2122

2223
type TreeNode = Readonly<{
@@ -51,19 +52,10 @@ describe('FixedSizeTree', () => {
5152
let treeWalkerSpy: jest.Mock;
5253
let isOpenByDefault: boolean;
5354

54-
const getNodeData = (
55+
let getNodeData: (
5556
node: TreeNode,
5657
nestingLevel: number,
57-
): TreeWalkerValue<ExtendedData, NodeMeta> => ({
58-
data: {
59-
id: node.id.toString(),
60-
isOpenByDefault,
61-
name: node.name,
62-
nestingLevel,
63-
},
64-
nestingLevel,
65-
node,
66-
});
58+
) => TreeWalkerValue<ExtendedData, NodeMeta>;
6759

6860
function* treeWalker(): ReturnType<TreeWalker<ExtendedData, NodeMeta>> {
6961
yield getNodeData(tree, 0);
@@ -105,6 +97,20 @@ describe('FixedSizeTree', () => {
10597

10698
isOpenByDefault = true;
10799

100+
getNodeData = (
101+
node: TreeNode,
102+
nestingLevel: number,
103+
): TreeWalkerValue<ExtendedData, NodeMeta> => ({
104+
data: {
105+
id: node.id.toString(),
106+
isOpenByDefault,
107+
name: node.name,
108+
nestingLevel,
109+
},
110+
nestingLevel,
111+
node,
112+
});
113+
108114
treeWalkerSpy = jest.fn(treeWalker);
109115

110116
component = mountComponent();
@@ -596,5 +602,82 @@ describe('FixedSizeTree', () => {
596602
list = component.find(FixedSizeList);
597603
expect(list.prop('itemCount')).toBe(7);
598604
});
605+
606+
it('correctly collapses node with 100.000 children', async () => {
607+
tree = treeWithLargeNode;
608+
component = mountComponent();
609+
610+
const records = extractReceivedRecords<
611+
FixedSizeListProps,
612+
ExtendedData,
613+
NodePublicState<ExtendedData>
614+
>(component.find(FixedSizeList));
615+
616+
const {setOpen} = records.find(
617+
(record) => record.data.id === 'largeNode-1',
618+
)!;
619+
620+
await setOpen(false);
621+
component.update(); // Update the wrapper to get the latest changes
622+
623+
const updatedRecords = extractReceivedRecords(
624+
component.find(FixedSizeList),
625+
);
626+
627+
expect(updatedRecords.map(({data: {id}}) => id)).toEqual([
628+
'root-1',
629+
'smallNode-1',
630+
'smallNodeChild-1',
631+
'smallNodeChild-2',
632+
'largeNode-1',
633+
'smallNode-2',
634+
'smallNodeChild-3',
635+
'smallNodeChild-4',
636+
]);
637+
});
638+
639+
it('correctly expands node with 100.000 children', async () => {
640+
getNodeData = (
641+
node: TreeNode,
642+
nestingLevel: number,
643+
): TreeWalkerValue<ExtendedData, NodeMeta> => ({
644+
data: {
645+
id: node.id.toString(),
646+
isOpenByDefault: node.id !== 'largeNode-1',
647+
name: node.name,
648+
nestingLevel,
649+
},
650+
nestingLevel,
651+
node,
652+
});
653+
tree = treeWithLargeNode;
654+
655+
component = mountComponent();
656+
657+
const records = extractReceivedRecords<
658+
FixedSizeListProps,
659+
ExtendedData,
660+
NodePublicState<ExtendedData>
661+
>(component.find(FixedSizeList));
662+
663+
const {setOpen} = records.find(
664+
(record) => record.data.id === 'largeNode-1',
665+
)!;
666+
667+
await setOpen(true);
668+
component.update(); // Update the wrapper to get the latest changes
669+
670+
const updatedRecords = extractReceivedRecords(
671+
component.find(FixedSizeList),
672+
);
673+
674+
expect(updatedRecords.slice(-5).map(({data: {id}}) => id)).toEqual([
675+
'largeNodeChild-99999',
676+
'largeNodeChild-100000',
677+
'smallNode-2',
678+
'smallNodeChild-3',
679+
'smallNodeChild-4',
680+
]);
681+
});
599682
});
600683
});

__tests__/utils/misc.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,44 @@ export const defaultTree = {
7878
id: 'foo-1',
7979
name: 'Foo #1',
8080
};
81+
82+
const getLargeSetOfChildren = () => {
83+
const children = [];
84+
85+
for (let i = 0; i < 100000; i++) {
86+
children.push({
87+
id: `largeNodeChild-${i + 1}`,
88+
name: `Large Node Child #${i + 1}`,
89+
});
90+
}
91+
92+
return children;
93+
};
94+
95+
export const treeWithLargeNode = {
96+
children: [
97+
{
98+
children: [
99+
{id: 'smallNodeChild-1', name: 'Small Node Child #1'},
100+
{id: 'smallNodeChild-2', name: 'Small Node Child #2'},
101+
],
102+
id: 'smallNode-1',
103+
name: 'Small Node #1',
104+
},
105+
{
106+
children: getLargeSetOfChildren(),
107+
id: 'largeNode-1',
108+
name: 'Large Node #1',
109+
},
110+
{
111+
children: [
112+
{id: 'smallNodeChild-3', name: 'Small Node Child #3'},
113+
{id: 'smallNodeChild-4', name: 'Small Node Child #4'},
114+
],
115+
id: 'smallNode-2',
116+
name: 'Small Node #2',
117+
},
118+
],
119+
id: 'root-1',
120+
name: 'Root #1',
121+
};

src/Tree.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,7 @@ const generateNewTree = <
386386
};
387387

388388
const MAX_FUNCTION_ARGUMENTS = 32768;
389+
const SPLICE_DEFAULT_ARGUMENTS_NUMBER = 2;
389390

390391
// If we need to perform only the update, treeWalker won't be used. Update will
391392
// work internally, traversing only the subtree of elements that require
@@ -456,8 +457,6 @@ const updateExistingTree = <
456457
[index + 1, countToRemove],
457458
];
458459

459-
let orderPartsCursor = 0;
460-
461460
// Unfortunately, splice cannot work with big arrays. If array exceeds
462461
// some length it may fire an exception. The length is specific for
463462
// each engine; e.g., MDN says about 65536 for Webkit. So, to avoid this
@@ -478,14 +477,15 @@ const updateExistingTree = <
478477
: true;
479478

480479
if (record.isShown) {
481-
orderParts[orderPartsCursor].push(record.public.data.id);
480+
const currentOrderPart = orderParts[orderParts.length - 1];
481+
currentOrderPart.push(record.public.data.id);
482482

483483
if (
484-
orderParts[orderPartsCursor].length === MAX_FUNCTION_ARGUMENTS
484+
currentOrderPart.length ===
485+
MAX_FUNCTION_ARGUMENTS + SPLICE_DEFAULT_ARGUMENTS_NUMBER
485486
) {
486-
orderPartsCursor += 1;
487487
orderParts.push([
488-
index + 1 + orderPartsCursor * MAX_FUNCTION_ARGUMENTS,
488+
index + 1 + MAX_FUNCTION_ARGUMENTS * orderParts.length,
489489
0,
490490
]);
491491
}

0 commit comments

Comments
 (0)