Paginated tree grid fails to display items if rapidly collapsed
Description
When using a TreeGrid with an AbstractHierarchicalDataProvider, the tree grid will fail to display nested items if the parent item is collapsed and expanded too quickly (i. e. faster than the data provider fetches the items). This appears to only happen if the nested item count exceeds the pagination size of the tree grid.
I've attached a screen recording of the minimal reproducible example.
https://github.com/user-attachments/assets/7e326b3e-a0ce-4ac6-921f-e9f59cc5dce2
Expected outcome
The parent item should be collapsed after all nested items have been fetched and expand afterwards if clicked again.
Minimal reproducible example
Example project here: treegridtest.zip
@Route(value = "tree-grid-issue", layout = MainLayout.class)
public class TreeGridIssue extends VerticalLayout {
public TreeGridIssue() {
setPadding(true);
setSpacing(true);
TreeGrid<String> treeGrid = new TreeGrid<>();
treeGrid.setSizeFull();
treeGrid.setPageSize(25);
treeGrid.setDataProvider(
new AbstractHierarchicalDataProvider<String, Object>() {
@Override
public boolean isInMemory() {
return false;
}
@Override
public int getChildCount(
HierarchicalQuery<String, Object> hierarchicalQuery) {
if (Objects.equals(hierarchicalQuery.getParent(),
"root")
|| Objects.equals(hierarchicalQuery.getParent(),
"root1")) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return 100;
} else if (hierarchicalQuery.getParent() == null) {
return 2;
} else {
return 0;
}
}
@Override
public Stream<String> fetchChildren(
HierarchicalQuery<String, Object> hierarchicalQuery) {
String parent = hierarchicalQuery.getParent();
if (parent == null) {
return Stream.of("root", "root1");
} else {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
var itemList = Stream
.iterate(1, i -> i <= 100, i -> i + 1)
.map(String::valueOf).toList();
var offset = hierarchicalQuery.getOffset();
var limit = hierarchicalQuery.getLimit();
if (offset >= itemList.size()) {
return Stream.empty();
}
return itemList.subList(offset,
Math.min(offset + limit, itemList.size()))
.stream();
}
}
@Override
public boolean hasChildren(String s) {
return s.equals("root") || s.equals("root1");
}
});
treeGrid.addHierarchyColumn(s -> s);
setSizeFull();
add(treeGrid);
}
}
Steps to reproduce
- Add a tree grid with a data provider to a page. The data provider should need some time to fetch the items, fetch some items one the root level and many items than the pageSize of the treeGrid on the second level.
- Quickly expand one item on the root level and collapse it (double click).
- Try expanding the item again => No items are loaded
Environment
Vaadin version(s): 24.7.6 OS: Windows 10
Browsers
Firefox, Chrome, ...
Additional observations.
- The issue reproduces when the second click to collapse is done before fetching of the items is completed.
- The issue does not reproduce if you first let the items to load and then collapse the tree, you can expand again.
- If you time the second click after the first page of items is rendered and TreeGrid continues loading next page, you will see
and Exception on the server log
Caused by: java.lang.IndexOutOfBoundsException: Index 25 out of bounds for length 25
at java.base/jdk.internal.util.Preconditions.outOfBounds(Preconditions.java:64) ~[na:na]
at java.base/jdk.internal.util.Preconditions.outOfBoundsCheckIndex(Preconditions.java:70) ~[na:na]
at java.base/jdk.internal.util.Preconditions.checkIndex(Preconditions.java:266) ~[na:na]
at java.base/java.util.Objects.checkIndex(Objects.java:361) ~[na:na]
at java.base/java.util.ArrayList.get(ArrayList.java:427) ~[na:na]
at com.vaadin.flow.data.provider.hierarchy.HierarchicalCommunicationController.lambda$getJsonItems$5(HierarchicalCommunicationController.java:335) ~[flow-data-24.7.5.jar:24.7.5]
at java.base/java.util.stream.IntPipeline$1$1.accept(IntPipeline.java:180) ~[na:na]
at java.base/java.util.stream.Streams$RangeIntSpliterator.forEachRemaining(Streams.java:104) ~[na:na]
at java.base/java.util.Spliterator$OfInt.forEachRemaining(Spliterator.java:711) ~[na:na]
at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509) ~[na:na]
at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499) ~[na:na]
at java.base/java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:921) ~[na:na]
at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234) ~[na:na]
at java.base/java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:682) ~[na:na]
at com.vaadin.flow.data.provider.hierarchy.HierarchicalCommunicationController.getJsonItems(HierarchicalCommunicationController.java:337) ~[flow-data-24.7.5.jar:24.7.5]
at com.vaadin.flow.data.provider.hierarchy.HierarchicalCommunicationController.set(HierarchicalCommunicationController.java:210) ~[flow-data-24.7.5.jar:24.7.5]
at com.vaadin.flow.data.provider.hierarchy.HierarchicalCommunicationController.lambda$collectChangesToSend$1(HierarchicalCommunicationController.java:194) ~[flow-data-24.7.5.jar:24.7.5]
at com.vaadin.flow.data.provider.hierarchy.HierarchicalCommunicationController.applyIfNotEmpty(HierarchicalCommunicationController.java:363) ~[flow-data-24.7.5.jar:24.7.5]
at com.vaadin.flow.data.provider.hierarchy.HierarchicalCommunicationController.withMissing(HierarchicalCommunicationController.java:357) ~[flow-data-24.7.5.jar:24.7.5]
at com.vaadin.flow.data.provider.hierarchy.HierarchicalCommunicationController.collectChangesToSend(HierarchicalCommunicationController.java:193) ~[flow-data-24.7.5.jar:24.7.5]
at com.vaadin.flow.data.provider.hierarchy.HierarchicalCommunicationController.flush(HierarchicalCommunicationController.java:137) ~[flow-data-24.7.5.jar:24.7.5]
at com.vaadin.flow.data.provider.hierarchy.HierarchicalDataCommunicator.lambda$requestFlush$e15592a2$1(HierarchicalDataCommunicator.java:118) ~[flow-data-24.7.5.jar:24.7.5]
at com.vaadin.flow.internal.StateTree.lambda$runExecutionsBeforeClientResponse$2(StateTree.java:399) ~[flow-server-24.7.5.jar:24.7.5]
at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:183) ~[na:na]
at java.base/java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:179) ~[na:na]
at java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1625) ~[na:na]
at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509) ~[na:na]
at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499) ~[na:na]
at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:150) ~[na:na]
at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:173) ~[na:na]
at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234) ~[na:na]
at java.base/java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:596) ~[na:na]
at com.vaadin.flow.internal.StateTree.runExecutionsBeforeClientResponse(StateTree.java:394) ~[flow-server-24.7.5.jar:24.7.5]
at com.vaadin.flow.server.communication.UidlWriter.encodeChanges(UidlWriter.java:394) ~[flow-server-24.7.5.jar:24.7.5]
at com.vaadin.flow.server.communication.UidlWriter.createUidl(UidlWriter.java:170) ~[flow-server-24.7.5.jar:24.7.5]
at com.vaadin.flow.server.communication.UidlWriter.createUidl(UidlWriter.java:215) ~[flow-server-24.7.5.jar:24.7.5]
at com.vaadin.flow.server.communication.AtmospherePushConnection.push(AtmospherePushConnection.java:207) ~[flow-server-24.7.5.jar:24.7.5]
... 33 common frames omitted
It looks like the root cause is in Flow hierarchical data communicator and controller logic regarding handling pending data requests on expand and collapse.
Resolved by https://github.com/vaadin/platform/issues/7843 in Vaadin 25.
Reopening, as this issue has been requested to be fixed in Vaadin 24 specifically.