flow icon indicating copy to clipboard operation
flow copied to clipboard

Paginated tree grid fails to display items if rapidly collapsed

Open timklge opened this issue 7 months ago • 4 comments

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

  1. 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.
  2. Quickly expand one item on the root level and collapse it (double click).
  3. Try expanding the item again => No items are loaded

Environment

Vaadin version(s): 24.7.6 OS: Windows 10

Browsers

Firefox, Chrome, ...

timklge avatar Jun 02 '25 12:06 timklge

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

Image

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

TatuLund avatar Jun 03 '25 06:06 TatuLund

It looks like the root cause is in Flow hierarchical data communicator and controller logic regarding handling pending data requests on expand and collapse.

ugur-vaadin avatar Jun 19 '25 11:06 ugur-vaadin

Resolved by https://github.com/vaadin/platform/issues/7843 in Vaadin 25.

vursen avatar Nov 17 '25 14:11 vursen

Reopening, as this issue has been requested to be fixed in Vaadin 24 specifically.

vursen avatar Nov 17 '25 17:11 vursen