Repository: vaadin/flow
Pull request #21189: feat: Enable preserving partial view chain
Author: login: caalador
Labels: name: +0.1.0
name: prerelease version for Vaadin 24.8.0
name: Released with Vaadin 24.8.0.beta1
Created: 2025-03-27T10:43:41Z | Merged: 2025-04-03T04:53:39Z

Make it possible to reuse only parts
of the preserve on refresh chain instead
of the full chain.

Closes #21139


Linked issue #21139: Allow PreserveOnRefresh when there's only a partial match to the preserved route chain
### Description of the bug

I have an app with a `MainLayout` annotated with `@PreserveAndRefresh` and two views, both annotated with `@Route @RouteScope @RouteScopeOwner(MainLayout.class) @SpringComponent`. I'm hoping to achieve browser-tab-scoped routes. All works okay when I navigate around the app via clicking on router links; also browser refresh is handled correctly.

However, when I type in the URL manually into browser's address bar, the `MainLayout` suddenly gets re-created, which then breaks the tab scope and causes new instances of all routes to be created.

This behavior is really strange since the `window.name` isn't changed (in Firefox). Note that the choice of the browser is extremely important - Safari, when you enter URL manually, changes the `window.name`, and in such case it's understandable to have new instances of `MainLayout` and the routes. However, the same issue is reproducible with Firefox as well, which doesn't change the `window.name`.

This issue can be reproduced as follows:
1. Click a link on browser's bookmark toolbar.
2. TestBench: call `getDriver().get("url")`.

The TestBench case is the most problematic in particular: if your app depends on tab scope to work properly, this bug prevents the app to be tested via `getDriver().get("url")` and you need to tell the TestBench-controlled browser to navigate via other means instead (e.g. via clicking links in a nav bar).

### Expected behavior

The `MainLayout` instance should be reused.

### Minimal reproducible example

Check out the https://github.com/vaadin/skeleton-starter-flow-spring example app for Vaadin 23.5.15. Then create the following classes:
```java
@PreserveOnRefresh
public class MainLayout extends Div implements RouterLayout {
    public MainLayout() {
        System.out.println(this + ": constructor");
    }

    @Override
    public void showRouterLayoutContent(HasElement content) {
        content.getElement().removeFromTree();
        RouterLayout.super.showRouterLayoutContent(content);
    }
}
@Route(layout = MainLayout.class)
@RouteScope
@RouteScopeOwner(MainLayout.class)
@SpringComponent
public class MainView extends VerticalLayout {
    public MainView() {
        System.out.println(this + ": constructor");
        UI.getCurrent().getPage().retrieveExtendedClientDetails(e -> System.out.println(e.getWindowName()));
        add(new RouterLink("Navigate to second view - this works correctly", SecondView.class));
    }
}
@Route(value = "second", layout = MainLayout.class)
@RouteScope
@RouteScopeOwner(MainLayout.class)
@SpringComponent
public class SecondView extends VerticalLayout {
    public SecondView() {
        System.out.println(this + ": constructor");
        UI.getCurrent().getPage().retrieveExtendedClientDetails(e -> System.out.println(e.getWindowName()));
    }
}
```
Run the app; the browser opens and navigates to http://localhost:8080 and the following is printed to Java stdout:
```
org.vaadin.example.MainLayout@26a9aa3e: constructor
org.vaadin.example.MainView@1cee63e2: constructor
ROOT-2521314-0.937907358684621
```
All good so far. Now enter the following into the browser's address bar manually, via keyboard: `http://localhost:8080/second`. This navigates to `SecondView`, however you can see in Java stdout that a new instance of `MainLayout` got instantiated, while the `window.name` stayed the same:
```
org.vaadin.example.MainLayout@4b4a7429: constructor
org.vaadin.example.SecondView@23d28536: constructor
ROOT-2521314-0.937907358684621
```


### Versions

- Vaadin / Flow version: 23.5.15
- Java version: 17 (doesn't matter)
- OS version: Ubuntu 24.10 (doesn't matter)
- Browser version (if applicable): Firefox 136.0.1

Linked issue comments: user: login: mshabarov
created_at: 2025-03-17T15:13:25Z
body: I couldn't reproduce this. If I change the layout logging to
```
public MainLayout() {
    System.out.println(this + ": constructor (main)");
}
```
I always get the following output:
```
org.vaadin.example.MainLayout@400c5895: constructor (main)
org.vaadin.example.MainView@5b16b6a6: constructor
ROOT-2521314-0.7581569797712584
org.vaadin.example.SecondView@102ad005: constructor
ROOT-2521314-0.7581569797712584
```

i.e. same `window.name` and no second call to main layout CTOR.
Tested in Chrome, Firefox, Safari.

author_association: CONTRIBUTOR
user: login: mvysny
created_at: 2025-03-17T17:40:18Z
body: Thank you for taking a look; let me prepare an example app and a recording which reproduces the issue. Tomorrow morning.
author_association: MEMBER
user: login: abdullahtellioglu
created_at: 2025-03-17T20:29:41Z
body: I created this issue long time ago https://github.com/vaadin/flow/issues/16833 and I  remember that something has changed with `PreserveOnRefresh` logic after that to align the behavior between browsers, if I am not mistaken 
author_association: COLLABORATOR
user: login: mvysny
created_at: 2025-03-18T05:43:02Z
body: Thanks @abdullahtellioglu . The issue #16833 is caused by `window.name` being changed by the browser - in such case indeed there's nothing Vaadin can do. However, in this case, the `window.name` isn't changed (it stays `ROOT-2521314-0.937907358684621`) yet the main layout gets re-created.

@mshabarov have you tested with Vaadin 23.5.15 please? I'm attaching an example project which reproduces the issue on my environment.

[skeleton-starter-flow-spring.zip](https://github.com/user-attachments/files/19307185/skeleton-starter-flow-spring.zip)

Screen recording incoming.
author_association: MEMBER
user: login: mvysny
created_at: 2025-03-18T05:53:47Z
body: https://github.com/user-attachments/assets/e738eaab-fc31-4879-9aa2-b36f33c6a588
https://github.com/user-attachments/assets/e20827bf-03ea-403f-ba89-cae565d1b22f

The second recording shows that the "oldChain" is empty, therefore there are no previous instances for flow to reuse and a new instance of MainLayout is therefore constructed. The question is, why is the "oldChain" empty? 🤔 
author_association: MEMBER
user: login: mshabarov
created_at: 2025-03-18T07:26:17Z
body: @mvysny my first test was with 24.6 indeed, but then I re-tested with 23.5.15 and got the same result (no reproduce),
here is the video recording:

https://github.com/user-attachments/assets/d8ca6722-700b-437e-849c-ab84e043b6b4

I'll debug it further.
author_association: CONTRIBUTOR
user: login: mvysny
created_at: 2025-03-18T07:33:43Z
body: @mshabarov thank you for the recording - thanks to the recording I found out why you can't reproduce the issue. If you click the link (as you did in the video), everything works okay and indeed a second instance of MainLayout isn't created. In order to reproduce the issue you must not click on the link, and instead do this: open the `MainView` as usual, but instead of clicking on the link, please type the URL http://localhost:8080/second into the address bar of your browser and press Enter.
author_association: MEMBER
user: login: mshabarov
created_at: 2025-03-18T07:52:41Z
body: Ah, my bad, sorry, the point is to open a bookmark or re-type the url by hand. Okay, now this is visible indeed.
author_association: CONTRIBUTOR
user: login: mshabarov
created_at: 2025-03-18T08:13:10Z
body: One more note: with 23.5.15 the `window.name` remains the same between main and second, but with 24.6 it changes, so it never stays the same between navigations, this is the pattern:
```
org.vaadin.example.MainLayout@605e37b2: constructor (main)
org.vaadin.example.MainView@53cd420f: constructor
ROOT-2521314-0.6930707085701084
org.vaadin.example.MainLayout@4b4d70bb: constructor (main)
org.vaadin.example.SecondView@2e1169e9: constructor
ROOT-2521314-0.7927226316008651
```
author_association: CONTRIBUTOR
user: login: mvysny
created_at: 2025-03-18T10:51:22Z
body: Yup, I've reproduced the changing `window.name` value on Safari. Filed as a separate ticket https://github.com/vaadin/flow/issues/21141
author_association: MEMBER
user: login: Legioth
created_at: 2025-03-18T16:09:48Z
body: Is this ticket still relevant in light of the findings from https://github.com/vaadin/flow/issues/21141?
author_association: MEMBER
user: login: mvysny
created_at: 2025-03-18T16:16:16Z
body: I believe it is. The difference is that in this ticket, the window.name is preserved and still the main layout gets re-created.
author_association: MEMBER
user: login: Legioth
created_at: 2025-03-18T17:02:31Z
body: I wonder if this is related to the more eager UI cleanup that we introduced at some point? Maybe the window close event finds it way to the server in a different way when navigating compared to a direct reload?

Or is it so that the preservation logic is also tied to the URL or view to avoid "false positives" from the point of view of only preserving for refreshes?
author_association: MEMBER
user: login: mcollovati
created_at: 2025-03-18T17:11:41Z
body: Probably I'm wrong, but it looks like to me that what happens is what we have documented in the "Preconditions and Limitations" paragraph

> The component is made to persist only when reloaded in the same browser tab — the window.name client-side property is used to identify the tab — and only if the URL stays the same. **Visiting another route or changing a URL parameter discards the component state permanently.**

> The annotation doesn’t support partial preserving. You can’t preserve only some components on the route chain. If the annotation is present on any component in the chain, the entire chain is preserved.

We are navigating to a different route, so the entire chain is discarded.
author_association: COLLABORATOR
user: login: mvysny
created_at: 2025-03-19T07:32:12Z
body: Yes. Technically, `@PreserveOnRefresh` works according to the spec and this bug report is invalid.
author_association: MEMBER
user: login: Legioth
created_at: 2025-03-19T07:57:33Z
body: There might be cases where it would be useful to preserve the parts of the chain that remains valid after a URL change. Would it make sense to add that as an opt-in feature, e.g. in the form of `@PreserveOnRefresh(preservePartialRouteChains = true)`?
author_association: MEMBER
user: login: mvysny
created_at: 2025-03-19T08:19:28Z
body: If this solution would preserve the main layout (and it sounds like it should), then it would solve my problem 🥳 
author_association: MEMBER
user: login: mvysny
created_at: 2025-03-19T11:11:30Z
body: Workaround for TestBench testing, as suggested by @Legioth . Instead of `getDriver().get()` you would call:

1. `getCommandExecutor().executeScript("document.location = arguments[0];", url);` but that doesn't work since Flow will perform a full reload.
2. This works: `getCommandExecutor().executeScript("history.pushState(null, null, arguments[0]); window.dispatchEvent(new PopStateEvent('popstate'));", url);`
author_association: MEMBER
user: login: mshabarov
created_at: 2025-03-20T07:49:42Z
body: A follow-up for https://github.com/vaadin/flow/issues/21139#issuecomment-2735643553:

The use case described in this ticket can be covered by a new feature:

**How it works now:**

1. `AbstractNavigationStateRenderer::hasPreservedChainOfLocation` checks the whole location path:
        
```java
final PreservedComponentCache cache = session
                .getAttribute(PreservedComponentCache.class);
return cache != null && cache.values().stream()
                .anyMatch(entry -> entry.getFirst().equals(location.getPath()));
```
2. It returns `false` in case of a new url in address bar and `getPreservedChain`, then it returns a chain with just one empty array element.
3. This in turn triggers `createChainIfEmptyAndExecuteBeforeEnterNavigation` and `sendBeforeEnterEventAndPopulateChain` methods being invoked, where Flow sees empty chain (present, but empty, not absent `Optional.empty()`) and instantiates layouts and route objects once again making `@PreserveOnRefresh` useless.

**Proposed solution:**
1. Before putting a new value into `PreservedComponentCache`, Flow could restore the old value by the key (`window.name`), compare it with the new value, find intersections, i.e. same classes that are in the value array of the old and new value. In the above example it's `MainLayout` object.
2. Then Flow puts the intersected classes into a chain and does not instantiate these classes once again, but only what else is in the new value array, e.g. `SecondView` in our case.
3. `AbstractNavigationStateRenderer::handle` needs changes, the cache class `PreservedComponentCache` can have the same structure, and store the same key/values, but the comparison should be done segment-by-segment or class-by-class, not with `entry.getFirst().equals(location.getPath()))`.
4. This feature should be opt-in based, Flow extends the existing preserve on refresh annotation with a new flag: `@PreserveOnRefresh(partialMatch = true)`. That will make layouts to be preserved when changing url in address bar.
author_association: CONTRIBUTOR
user: login: mvysny
created_at: 2025-03-20T08:00:04Z
body: That sounds great and would solve the issue. Thanks @mshabarov  🙏 
author_association: MEMBER

Diff:
diff --git a/flow-server/src/main/java/com/vaadin/flow/component/internal/UIInternals.java b/flow-server/src/main/java/com/vaadin/flow/component/internal/UIInternals.java
index c9cabe553e3..fbc90c1fb3a 100644
--- a/flow-server/src/main/java/com/vaadin/flow/component/internal/UIInternals.java
+++ b/flow-server/src/main/java/com/vaadin/flow/component/internal/UIInternals.java
@@ -807,6 +807,19 @@ public boolean cancelPendingTitleUpdate() {
         return result;
     }
 
+    /**
+     * Populate the routerTargetChain with RouterLayouts, but only if the target
+     * chain is empty. If the chain contains elements the given list is ignored.
+     *
+     * @param layouts
+     *            stored router target chain to set as last navigated chain
+     */
+    public void setRouterTargetChain(List<RouterLayout> layouts) {
+        if (routerTargetChain.isEmpty()) {
+            routerTargetChain.addAll(layouts);
+        }
+    }
+
     /**
      * Shows a route target in the related UI. This method is intended for
      * framework use only. Use {@link UI#navigate(String)} to change the route
diff --git a/flow-server/src/main/java/com/vaadin/flow/router/PreserveOnRefresh.java b/flow-server/src/main/java/com/vaadin/flow/router/PreserveOnRefresh.java
index 0b77e87357b..fe37103cecf 100644
--- a/flow-server/src/main/java/com/vaadin/flow/router/PreserveOnRefresh.java
+++ b/flow-server/src/main/java/com/vaadin/flow/router/PreserveOnRefresh.java
@@ -45,4 +45,19 @@
 @Inherited
 @Documented
 public @interface PreserveOnRefresh {
+
+    /**
+     * Set to true if refresh should also reuse partial chain components of
+     * stored view chain.
+     * <p>
+     * This means that when navigating from a preserve on refresh target to a
+     * new url in the same client window context, where windowName matches, the
+     * router layouts that have been preserved will be reused without
+     * re-creation for the new route.
+     * <p>
+     * Default is {@code false} so only url match is repopulated.
+     *
+     * @return {@code true} if partial chain match should be checked and used
+     */
+    boolean partialMatch() default false;
 }
diff --git a/flow-server/src/main/java/com/vaadin/flow/router/internal/AbstractNavigationStateRenderer.java b/flow-server/src/main/java/com/vaadin/flow/router/internal/AbstractNavigationStateRenderer.java
index 8d23b7125a9..ab71c262f02 100644
--- a/flow-server/src/main/java/com/vaadin/flow/router/internal/AbstractNavigationStateRenderer.java
+++ b/flow-server/src/main/java/com/vaadin/flow/router/internal/AbstractNavigationStateRenderer.java
@@ -129,7 +129,7 @@ public NavigationState getNavigationState() {
      */
     @SuppressWarnings("unchecked")
     // Non-private for testing purposes
-    static <T extends HasElement> T getRouteTarget(Class<T> routeTargetType,
+    <T extends HasElement> T getRouteTarget(Class<T> routeTargetType,
             NavigationEvent event, boolean lastElement) {
         UI ui = event.getUI();
         Instantiator instantiator = Instantiator.get(ui);
@@ -267,6 +267,48 @@ private boolean populateChain(ArrayList<HasElement> chain,
                 return true;
             }
             chain.addAll(maybeChain.get());
+
+            // If partialMatch is set to true check if the cache contains a
+            // chain and possibly request extended details to get window name
+            // to select cached chain.
+            if (chain.isEmpty() && isPreservePartialTarget(
+                    navigationState.getNavigationTarget(), routeLayoutTypes)) {
+                UI ui = event.getUI();
+                if (ui.getInternals().getExtendedClientDetails() == null) {
+                    PreservedComponentCache cache = ui.getSession()
+                            .getAttribute(PreservedComponentCache.class);
+                    if (cache != null && !cache.isEmpty()) {
+                        // As there is a cached chain we get the client details
+                        // to get the window name so we can determine if the
+                        // cache contains a chain for us to use.
+                        ui.getPage().retrieveExtendedClientDetails(
+                                details -> handle(event));
+                        return true;
+                    }
+                } else {
+                    Optional<List<HasElement>> partialChain = getWindowPreservedChain(
+                            ui.getSession(),
+                            ui.getInternals().getExtendedClientDetails()
+                                    .getWindowName());
+                    if (partialChain.isPresent()) {
+                        List<HasElement> oldChain = partialChain.get();
+                        disconnectElements(oldChain, ui);
+
+                        List<RouterLayout> routerLayouts = new ArrayList<>();
+
+                        for (HasElement hasElement : oldChain) {
+                            if (hasElement instanceof RouterLayout) {
+                                routerLayouts.add((RouterLayout) hasElement);
+                            } else {
+                                // Remove any non element from their parent to
+                                // not get old or duplicate route content
+                                hasElement.getElement().removeFromParent();
+                            }
+                        }
+                        ui.getInternals().setRouterTargetChain(routerLayouts);
+                    }
+                }
+            }
         } else {
             // Create an empty chain which gets populated later in
             // `createChainIfEmptyAndExecuteBeforeEnterNavigation`.
@@ -966,23 +1008,7 @@ private Optional<ArrayList<HasElement>> getPreservedChain(
             if (maybePreserved.isPresent()) {
                 // Re-use preserved chain for this route
                 ArrayList<HasElement> chain = maybePreserved.get();
-                final HasElement root = chain.get(chain.size() - 1);
-                final Component component = (Component) chain.get(0);
-                final Optional<UI> maybePrevUI = component.getUI();
-
-                if (maybePrevUI.isPresent() && maybePrevUI.get().equals(ui)) {
-                    return Optional.of(chain);
-                }
-
-                // Remove the top-level component from the tree
-                root.getElement().removeFromTree(false);
-
-                // Transfer all remaining UI child elements (typically dialogs
-                // and notifications) to the new UI
-                maybePrevUI.ifPresent(prevUi -> {
-                    ui.getInternals().moveElementsFrom(prevUi);
-                    prevUi.close();
-                });
+                disconnectElements(chain, ui);
 
                 return Optional.of(chain);
             }
@@ -991,6 +1017,26 @@ private Optional<ArrayList<HasElement>> getPreservedChain(
         return Optional.of(new ArrayList<>(0));
     }
 
+    private static void disconnectElements(List<HasElement> chain, UI ui) {
+        final HasElement root = chain.get(chain.size() - 1);
+        final Component component = (Component) chain.get(0);
+        final Optional<UI> maybePrevUI = component.getUI();
+
+        if (maybePrevUI.isPresent() && maybePrevUI.get().equals(ui)) {
+            return;
+        }
+
+        // Remove the top-level component from the tree
+        root.getElement().removeFromTree(false);
+
+        // Transfer all remaining UI child elements (typically dialogs
+        // and notifications) to the new UI
+        maybePrevUI.ifPresent(prevUi -> {
+            ui.getInternals().moveElementsFrom(prevUi);
+            prevUi.close();
+        });
+    }
+
     /**
      * Invoke this method with the chain that needs to be preserved after
      * {@link #handle(NavigationEvent)} method created it.
@@ -1079,6 +1125,18 @@ private static boolean isPreserveOnRefreshTarget(
                         .isAnnotationPresent(PreserveOnRefresh.class));
     }
 
+    private static boolean isPreservePartialTarget(
+            Class<? extends Component> routeTargetType,
+            List<Class<? extends RouterLayout>> routeLayoutTypes) {
+        return (routeTargetType.isAnnotationPresent(PreserveOnRefresh.class)
+                && routeTargetType.getAnnotation(PreserveOnRefresh.class)
+                        .partialMatch())
+                || routeLayoutTypes.stream().anyMatch(layoutType -> layoutType
+                        .isAnnotationPresent(PreserveOnRefresh.class)
+                        && layoutType.getAnnotation(PreserveOnRefresh.class)
+                                .partialMatch());
+    }
+
     // maps window.name to (location, chain)
     private static class PreservedComponentCache
             extends HashMap<String, Pair<String, ArrayList<HasElement>>> {
@@ -1105,9 +1163,27 @@ static Optional<ArrayList<HasElement>> getPreservedChain(
         if (cache != null && cache.containsKey(windowName) && cache
                 .get(windowName).getFirst().equals(location.getPath())) {
             return Optional.of(cache.get(windowName).getSecond());
-        } else {
-            return Optional.empty();
         }
+        return Optional.empty();
+    }
+
+    /**
+     * Get a preserved chain by window name only ignoring location path.
+     *
+     * @param session
+     *            current session
+     * @param windowName
+     *            window name to get cached view stack for
+     * @return view stack cache if available for window name
+     */
+    static Optional<List<HasElement>> getWindowPreservedChain(
+            VaadinSession session, String windowName) {
+        final PreservedComponentCache cache = session
+                .getAttribute(PreservedComponentCache.class);
+        if (cache != null && cache.containsKey(windowName)) {
+            return Optional.of(cache.get(windowName).getSecond());
+        }
+        return Optional.empty();
     }
 
     static void setPreservedChain(VaadinSession session, String windowName,
diff --git a/flow-tests/test-misc/src/main/java/com/vaadin/flow/misc/ui/partial/MainLayout.java b/flow-tests/test-misc/src/main/java/com/vaadin/flow/misc/ui/partial/MainLayout.java
new file mode 100644
index 00000000000..241d076287b
--- /dev/null
+++ b/flow-tests/test-misc/src/main/java/com/vaadin/flow/misc/ui/partial/MainLayout.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2000-2025 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.vaadin.flow.misc.ui.partial;
+
+import com.vaadin.flow.component.html.Div;
+import com.vaadin.flow.component.html.NativeButton;
+import com.vaadin.flow.router.ParentLayout;
+import com.vaadin.flow.router.RouterLayout;
+
+@ParentLayout(RootLayout.class)
+public class MainLayout extends Div implements RouterLayout {
+
+    public static final String EVENT_LOG_ID = "event-log";
+    public static final String RESET_ID = "reset-log";
+
+    private static int eventCounter = 0;
+
+    private final Div log = new Div();
+
+    public MainLayout() {
+        log.setText(++eventCounter + ": " + getClass().getSimpleName()
+                + ": constructor");
+        log.setId(EVENT_LOG_ID);
+        NativeButton reset = new NativeButton("Reset count",
+                e -> eventCounter = 0);
+        reset.setId(RESET_ID);
+        add(log, reset);
+    }
+}
diff --git a/flow-tests/test-misc/src/main/java/com/vaadin/flow/misc/ui/partial/MainView.java b/flow-tests/test-misc/src/main/java/com/vaadin/flow/misc/ui/partial/MainView.java
new file mode 100644
index 00000000000..055030f0fab
--- /dev/null
+++ b/flow-tests/test-misc/src/main/java/com/vaadin/flow/misc/ui/partial/MainView.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2000-2025 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.vaadin.flow.misc.ui.partial;
+
+import com.vaadin.flow.component.html.Div;
+import com.vaadin.flow.router.Route;
+import com.vaadin.flow.router.RouterLink;
+
+@Route(value = "main", layout = MainLayout.class)
+public class MainView extends Div {
+
+    public MainView() {
+
+        add(new RouterLink("Navigate to second view - this works correctly",
+                SecondView.class));
+    }
+}
diff --git a/flow-tests/test-misc/src/main/java/com/vaadin/flow/misc/ui/partial/RootLayout.java b/flow-tests/test-misc/src/main/java/com/vaadin/flow/misc/ui/partial/RootLayout.java
new file mode 100644
index 00000000000..062c7bb0265
--- /dev/null
+++ b/flow-tests/test-misc/src/main/java/com/vaadin/flow/misc/ui/partial/RootLayout.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2000-2025 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.vaadin.flow.misc.ui.partial;
+
+import com.vaadin.flow.component.html.Div;
+import com.vaadin.flow.component.html.NativeButton;
+import com.vaadin.flow.router.PreserveOnRefresh;
+import com.vaadin.flow.router.RouterLayout;
+
+@PreserveOnRefresh(partialMatch = true)
+public class RootLayout extends Div implements RouterLayout {
+
+    public static final String ROOT_EVENT_LOG_ID = "root-event-log";
+    public static final String ROOT_RESET_ID = "root-reset-log";
+
+    private static int eventCounter = 0;
+
+    private final Div log = new Div();
+
+    public RootLayout() {
+        log.setText(++eventCounter + ": " + getClass().getSimpleName()
+                + ": constructor");
+        log.setId(ROOT_EVENT_LOG_ID);
+        NativeButton reset = new NativeButton("Reset count",
+                e -> eventCounter = 0);
+        reset.setId(ROOT_RESET_ID);
+        add(log, reset);
+    }
+}
diff --git a/flow-tests/test-misc/src/main/java/com/vaadin/flow/misc/ui/partial/SecondView.java b/flow-tests/test-misc/src/main/java/com/vaadin/flow/misc/ui/partial/SecondView.java
new file mode 100644
index 00000000000..c7b8f583bda
--- /dev/null
+++ b/flow-tests/test-misc/src/main/java/com/vaadin/flow/misc/ui/partial/SecondView.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2000-2025 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.vaadin.flow.misc.ui.partial;
+
+import com.vaadin.flow.component.html.Div;
+import com.vaadin.flow.router.Route;
+
+@Route(value = "second", layout = MainLayout.class)
+public class SecondView extends Div {
+    public static final String SECOND_ID = "hello-second";
+
+    public SecondView() {
+        Div helloSecond = new Div("Hello second");
+        helloSecond.setId(SECOND_ID);
+        add(helloSecond);
+    }
+}
diff --git a/flow-tests/test-misc/src/test/java/com/vaadin/flow/misc/ui/PartialMatchRefreshIT.java b/flow-tests/test-misc/src/test/java/com/vaadin/flow/misc/ui/PartialMatchRefreshIT.java
new file mode 100644
index 00000000000..8fb83b6cc85
--- /dev/null
+++ b/flow-tests/test-misc/src/test/java/com/vaadin/flow/misc/ui/PartialMatchRefreshIT.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2000-2025 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.vaadin.flow.misc.ui;
+
+import net.jcip.annotations.NotThreadSafe;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Test;
+
+import com.vaadin.flow.component.html.testbench.AnchorElement;
+import com.vaadin.flow.component.html.testbench.DivElement;
+import com.vaadin.flow.component.html.testbench.NativeButtonElement;
+import com.vaadin.flow.testutil.ChromeBrowserTest;
+
+import static com.vaadin.flow.misc.ui.partial.MainLayout.EVENT_LOG_ID;
+import static com.vaadin.flow.misc.ui.partial.MainLayout.RESET_ID;
+import static com.vaadin.flow.misc.ui.partial.RootLayout.ROOT_EVENT_LOG_ID;
+import static com.vaadin.flow.misc.ui.partial.SecondView.SECOND_ID;
+
+@NotThreadSafe
+public class PartialMatchRefreshIT extends ChromeBrowserTest {
+
+    @Test
+    public void whenUpdatingUrl_parentChainShouldBeReused() {
+        open();
+
+        Assert.assertEquals("1: RootLayout: constructor",
+                $(DivElement.class).id(ROOT_EVENT_LOG_ID).getText());
+
+        Assert.assertEquals("1: MainLayout: constructor",
+                $(DivElement.class).id(EVENT_LOG_ID).getText());
+        Assert.assertEquals("Main navigation link should be available", 1,
+                $(AnchorElement.class).all().size());
+
+        getDriver().get(getRootURL() + "/second");
+
+        Assert.assertTrue("Couldn't find second view text div",
+                $(DivElement.class).id(SECOND_ID).isDisplayed());
+
+        Assert.assertEquals("1: RootLayout: constructor",
+                $(DivElement.class).id(ROOT_EVENT_LOG_ID).getText());
+        Assert.assertEquals("1: MainLayout: constructor",
+                $(DivElement.class).id(EVENT_LOG_ID).getText());
+
+    }
+
+    @After
+    public void tearDown() {
+        $(NativeButtonElement.class).id(RESET_ID).click();
+    }
+
+    @Override
+    protected String getTestPath() {
+        return "/main";
+    }
+
+}
diff --git a/flow-tests/vaadin-spring-tests/test-spring-boot/src/main/java/com/vaadin/flow/spring/test/partial/MainLayout.java b/flow-tests/vaadin-spring-tests/test-spring-boot/src/main/java/com/vaadin/flow/spring/test/partial/MainLayout.java
new file mode 100644
index 00000000000..ec2f4933ef6
--- /dev/null
+++ b/flow-tests/vaadin-spring-tests/test-spring-boot/src/main/java/com/vaadin/flow/spring/test/partial/MainLayout.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2000-2025 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.vaadin.flow.spring.test.partial;
+
+import com.vaadin.flow.component.html.Div;
+import com.vaadin.flow.component.html.NativeButton;
+import com.vaadin.flow.router.ParentLayout;
+import com.vaadin.flow.router.RouterLayout;
+
+@ParentLayout(RootLayout.class)
+public class MainLayout extends Div implements RouterLayout {
+
+    public static final String EVENT_LOG_ID = "event-log";
+    public static final String RESET_ID = "reset-log";
+
+    private static int eventCounter = 0;
+
+    private final Div log = new Div();
+
+    public MainLayout() {
+        log.setText(++eventCounter + ": " + getClass().getSimpleName()
+                + ": constructor");
+        log.setId(EVENT_LOG_ID);
+        NativeButton reset = new NativeButton("Reset count",
+                e -> eventCounter = 0);
+        reset.setId(RESET_ID);
+        add(log, reset);
+    }
+}
diff --git a/flow-tests/vaadin-spring-tests/test-spring-boot/src/main/java/com/vaadin/flow/spring/test/partial/MainView.java b/flow-tests/vaadin-spring-tests/test-spring-boot/src/main/java/com/vaadin/flow/spring/test/partial/MainView.java
new file mode 100644
index 00000000000..eda2706922b
--- /dev/null
+++ b/flow-tests/vaadin-spring-tests/test-spring-boot/src/main/java/com/vaadin/flow/spring/test/partial/MainView.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2000-2025 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.vaadin.flow.spring.test.partial;
+
+import com.vaadin.flow.component.html.Div;
+import com.vaadin.flow.router.Route;
+import com.vaadin.flow.router.RouterLink;
+import com.vaadin.flow.spring.annotation.RouteScope;
+import com.vaadin.flow.spring.annotation.RouteScopeOwner;
+import com.vaadin.flow.spring.annotation.SpringComponent;
+
+@Route(value = "main", layout = MainLayout.class)
+@RouteScope
+@RouteScopeOwner(MainLayout.class)
+@SpringComponent
+public class MainView extends Div {
+
+    public MainView() {
+
+        add(new RouterLink("Navigate to second view - this works correctly",
+                SecondView.class));
+    }
+}
diff --git a/flow-tests/vaadin-spring-tests/test-spring-boot/src/main/java/com/vaadin/flow/spring/test/partial/RootLayout.java b/flow-tests/vaadin-spring-tests/test-spring-boot/src/main/java/com/vaadin/flow/spring/test/partial/RootLayout.java
new file mode 100644
index 00000000000..4f2713a32d6
--- /dev/null
+++ b/flow-tests/vaadin-spring-tests/test-spring-boot/src/main/java/com/vaadin/flow/spring/test/partial/RootLayout.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2000-2025 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.vaadin.flow.spring.test.partial;
+
+import com.vaadin.flow.component.html.Div;
+import com.vaadin.flow.component.html.NativeButton;
+import com.vaadin.flow.router.PreserveOnRefresh;
+import com.vaadin.flow.router.RouterLayout;
+
+@PreserveOnRefresh(partialMatch = true)
+public class RootLayout extends Div implements RouterLayout {
+
+    public static final String ROOT_EVENT_LOG_ID = "root-event-log";
+    public static final String ROOT_RESET_ID = "root-reset-log";
+
+    private static int eventCounter = 0;
+
+    private final Div log = new Div();
+
+    public RootLayout() {
+        log.setText(++eventCounter + ": " + getClass().getSimpleName()
+                + ": constructor");
+        log.setId(ROOT_EVENT_LOG_ID);
+        NativeButton reset = new NativeButton("Reset count",
+                e -> eventCounter = 0);
+        reset.setId(ROOT_RESET_ID);
+        add(log, reset);
+    }
+}
diff --git a/flow-tests/vaadin-spring-tests/test-spring-boot/src/main/java/com/vaadin/flow/spring/test/partial/SecondView.java b/flow-tests/vaadin-spring-tests/test-spring-boot/src/main/java/com/vaadin/flow/spring/test/partial/SecondView.java
new file mode 100644
index 00000000000..a1b63c0bc9a
--- /dev/null
+++ b/flow-tests/vaadin-spring-tests/test-spring-boot/src/main/java/com/vaadin/flow/spring/test/partial/SecondView.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2000-2025 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.vaadin.flow.spring.test.partial;
+
+import com.vaadin.flow.component.html.Div;
+import com.vaadin.flow.router.Route;
+import com.vaadin.flow.spring.annotation.RouteScope;
+import com.vaadin.flow.spring.annotation.RouteScopeOwner;
+import com.vaadin.flow.spring.annotation.SpringComponent;
+
+@Route(value = "second", layout = MainLayout.class)
+@RouteScope
+@RouteScopeOwner(MainLayout.class)
+@SpringComponent
+public class SecondView extends Div {
+    public static final String SECOND_ID = "hello-second";
+
+    public SecondView() {
+        Div helloSecond = new Div("Hello second");
+        helloSecond.setId(SECOND_ID);
+        add(helloSecond);
+    }
+}
diff --git a/flow-tests/vaadin-spring-tests/test-spring-boot/src/test/java/com/vaadin/flow/spring/test/partial/PartialMatchRefreshIT.java b/flow-tests/vaadin-spring-tests/test-spring-boot/src/test/java/com/vaadin/flow/spring/test/partial/PartialMatchRefreshIT.java
new file mode 100644
index 00000000000..34f8f22ec6e
--- /dev/null
+++ b/flow-tests/vaadin-spring-tests/test-spring-boot/src/test/java/com/vaadin/flow/spring/test/partial/PartialMatchRefreshIT.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2000-2025 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.vaadin.flow.spring.test.partial;
+
+import net.jcip.annotations.NotThreadSafe;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Test;
+
+import com.vaadin.flow.component.html.testbench.AnchorElement;
+import com.vaadin.flow.component.html.testbench.DivElement;
+import com.vaadin.flow.component.html.testbench.NativeButtonElement;
+import com.vaadin.flow.testutil.ChromeBrowserTest;
+
+import static com.vaadin.flow.spring.test.partial.MainLayout.RESET_ID;
+import static com.vaadin.flow.spring.test.partial.MainLayout.EVENT_LOG_ID;
+import static com.vaadin.flow.spring.test.partial.RootLayout.ROOT_EVENT_LOG_ID;
+import static com.vaadin.flow.spring.test.partial.SecondView.SECOND_ID;
+
+@NotThreadSafe
+public class PartialMatchRefreshIT extends ChromeBrowserTest {
+
+    @Test
+    public void whenUpdatingUrl_parentChainShouldBeReused() {
+        open();
+
+        Assert.assertEquals("1: RootLayout: constructor",
+                $(DivElement.class).id(ROOT_EVENT_LOG_ID).getText());
+        Assert.assertEquals("1: MainLayout: constructor",
+                $(DivElement.class).id(EVENT_LOG_ID).getText());
+        Assert.assertEquals("Main navigation link should be available", 1,
+                $(AnchorElement.class).all().size());
+
+        getDriver().get(getRootURL() + "/second");
+
+        Assert.assertTrue("Couldn't find second view text div",
+                $(DivElement.class).id(SECOND_ID).isDisplayed());
+
+        Assert.assertEquals("1: MainLayout: constructor",
+                $(DivElement.class).id(EVENT_LOG_ID).getText());
+        Assert.assertEquals("1: RootLayout: constructor",
+                $(DivElement.class).id(ROOT_EVENT_LOG_ID).getText());
+    }
+
+    @After
+    public void tearDown() {
+        $(NativeButtonElement.class).id(RESET_ID).click();
+    }
+
+    @Override
+    protected String getTestPath() {
+        return "/main";
+    }
+}


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR introduces support for preserving only part of the view chain during refresh, enabling partial reuse of component hierarchies between navigations. Key changes include:  
- New test cases and sample views (MainView, SecondView, PartialMatchRefresh) to verify and demonstrate partial chain preservation.  
- Enhancements in AbstractNavigationStateRenderer to add and utilize a partial chain for navigation handling.  
- Extension of the PreserveOnRefresh annotation to support a new partialMatch property.

### Reviewed Changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.

<details>
<summary>Show a summary per file</summary>

| File | Description |
| ---- | ----------- |
| flow-tests/*PartialMatchRefresh.java | Adds test coverage for partial chain refresh in Vaadin Spring integration. |
| flow-tests/*SecondView.java | Updates route and preservation settings for the second view. |
| flow-tests/*MainView.java | Registers a router link to the second view. |
| flow-tests/*MainLayout.java | Enables and logs partial chain refresh behavior. |
| test-misc/* | Mirrors changes in the main test suite for alternate module testing. |
| flow-server/AbstractNavigationStateRenderer.java | Implements partial chain support, including client details retrieval and chain disconnection logic. |
| flow-server/PreserveOnRefresh.java | Adds documentation and a new property for partial chain matching. |
</details>



<details>
<summary>Comments suppressed due to low confidence (1)</summary>

**flow-server/src/main/java/com/vaadin/flow/router/internal/AbstractNavigationStateRenderer.java:1025**
* Add a guard clause to ensure that 'chain' is not empty before accessing its last element, to prevent possible IndexOutOfBoundsExceptions.
```
final HasElement root = chain.get(chain.size() - 1);
```
</details>


state: COMMENTED
author_association: NONE
submitted_at: 2025-03-27T14:22:34Z
user: login: caalador
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-28T05:19:24Z
user: login: mshabarov
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T12:34:39Z
user: login: mshabarov
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T13:02:55Z
user: login: mshabarov
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T13:07:56Z
user: login: caalador
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T09:46:43Z
user: login: mshabarov
body: The change looks good and now works as expected.
Though, could you please check if changes in https://github.com/vaadin/flow/pull/21211 make sense to apply:
1. Add javadoc
2. remove comment
3. move spring test folder to `src` from `main` (wrong location)
4. add a test that navigates with router link.
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T13:54:09Z
user: login: mshabarov
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T05:33:40Z
user: login: mshabarov
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T06:16:37Z

Review comments:
diff_hunk: @@ -1110,6 +1173,26 @@ static Optional<ArrayList<HasElement>> getPreservedChain(
         }
     }
 
+    /**
+     * Get a preserved chain by window name only ignoring location path.
+     *
+     * @param session
+     *            current session
+     * @param windowName
+     *            window name to get cached view stack for
+     * @return view stack cache if available for window name
+     */
+    static List<HasElement> getWindowPreservedChain(VaadinSession session,
+            String windowName) {
+        final PreservedComponentCache cache = session
+                .getAttribute(PreservedComponentCache.class);
+        if (cache != null && cache.containsKey(windowName)) {
+            return cache.get(windowName).getSecond();
+        } else {
+            return null;
path: flow-server/src/main/java/com/vaadin/flow/router/internal/AbstractNavigationStateRenderer.java
user: login: Copilot
body: Consider returning an empty list (or Optional.empty()) instead of null in getWindowPreservedChain() to avoid potential null-pointer issues downstream.
```suggestion
            return Collections.emptyList();
```
created_at: 2025-03-27T14:22:34Z
author_association: NONE
diff_hunk: @@ -1110,6 +1173,26 @@ static Optional<ArrayList<HasElement>> getPreservedChain(
         }
     }
 
+    /**
+     * Get a preserved chain by window name only ignoring location path.
+     *
+     * @param session
+     *            current session
+     * @param windowName
+     *            window name to get cached view stack for
+     * @return view stack cache if available for window name
+     */
+    static List<HasElement> getWindowPreservedChain(VaadinSession session,
+            String windowName) {
+        final PreservedComponentCache cache = session
+                .getAttribute(PreservedComponentCache.class);
+        if (cache != null && cache.containsKey(windowName)) {
+            return cache.get(windowName).getSecond();
+        } else {
+            return null;
path: flow-server/src/main/java/com/vaadin/flow/router/internal/AbstractNavigationStateRenderer.java
user: login: caalador
body: The rest of the feature explicitly handles null for no stored chain. Returning emptyList would require more checks and execute more code.
created_at: 2025-03-28T05:19:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -45,4 +45,14 @@
 @Inherited
 @Documented
 public @interface PreserveOnRefresh {
+
+    /**
+     * Set to true if refresh should also reuse partial chain components of
+     * stored view chain.
path: flow-server/src/main/java/com/vaadin/flow/router/PreserveOnRefresh.java
user: login: mshabarov
body: "Set to true if refresh should also reuse partial chain components of
stored view chain, e.g. preserving layout component instances whose content and sub-urls are not changed, but only a route class that changed and re-created."
created_at: 2025-03-28T14:20:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1110,6 +1173,26 @@ static Optional<ArrayList<HasElement>> getPreservedChain(
         }
     }
 
+    /**
+     * Get a preserved chain by window name only ignoring location path.
+     *
+     * @param session
+     *            current session
+     * @param windowName
+     *            window name to get cached view stack for
+     * @return view stack cache if available for window name
+     */
+    static List<HasElement> getWindowPreservedChain(VaadinSession session,
+            String windowName) {
+        final PreservedComponentCache cache = session
+                .getAttribute(PreservedComponentCache.class);
+        if (cache != null && cache.containsKey(windowName)) {
+            return cache.get(windowName).getSecond();
+        } else {
+            return null;
path: flow-server/src/main/java/com/vaadin/flow/router/internal/AbstractNavigationStateRenderer.java
user: login: mshabarov
body: Would be nice to return `Optional<ArrayList<HasElement>>` same as in other existing method `getPreservedChain`, but not blocking.
created_at: 2025-03-31T10:17:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,33 @@
+/*
+ * Copyright 2000-2025 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.vaadin.flow.misc.ui.partial;
+
+import com.vaadin.flow.component.html.Div;
+import com.vaadin.flow.router.PreserveOnRefresh;
+import com.vaadin.flow.router.Route;
+import com.vaadin.flow.router.RouterLink;
+
+@Route(value = "main", layout = MainLayout.class)
+@PreserveOnRefresh
path: flow-tests/test-misc/src/main/java/com/vaadin/flow/misc/ui/partial/MainView.java
user: login: mshabarov
body: Let's drop annotations on the routes and keep only layout annotations. Or, as you add two test modules, we could leave one with a single annotation and another with multiple annotations. 
created_at: 2025-03-31T11:57:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -267,6 +279,41 @@ private boolean populateChain(ArrayList<HasElement> chain,
                 return true;
             }
             chain.addAll(maybeChain.get());
+
+            // If partialMatch is set to true check if the cache contains a
+            // chain and possibly request extended details to get window name
+            // to select cached chain.
+            if (chain.isEmpty() && isPreservePartialTarget(
+                    navigationState.getNavigationTarget(), routeLayoutTypes)) {
+                UI ui = event.getUI();
+                if (ui.getInternals().getExtendedClientDetails() == null) {
+                    PreservedComponentCache cache = ui.getSession()
+                            .getAttribute(PreservedComponentCache.class);
+                    if (cache != null && !cache.isEmpty()) {
+                        // As there is a cached chain we get the client details
+                        // to get the window name so we can determine if the
+                        // cache contains a chain for us to use.
+                        ui.getPage().retrieveExtendedClientDetails(
+                                details -> handle(event));
+                        return true;
+                    }
+                } else {
+                    partialChain = getWindowPreservedChain(ui.getSession(),
+                            ui.getInternals().getExtendedClientDetails()
+                                    .getWindowName());
+                    if (partialChain != null) {
+                        disconnectElements(partialChain, ui);
+                        // Remove all router layout contents as parts are
+                        // reused and there is no old chain to remove against.
+                        for (int i = 0; i < partialChain.size() - 1; i++) {
+                            HasElement child = partialChain.get(i);
+                            RouterLayout parent = (RouterLayout) partialChain
+                                    .get(i + 1);
+                            parent.removeRouterLayoutContent(child);
+                        }
path: flow-server/src/main/java/com/vaadin/flow/router/internal/AbstractNavigationStateRenderer.java
user: login: mshabarov
body: Not sure I get why this cleanup is needed.

When I navigate by changing the url by hand, this works, but If I navigate from main view to second view via RouterLink, this basically erases all contents up to the main layout, so no content for a view nor for children layouts remains visible.

Can be reproduced with https://github.com/mshabarov/skeleton-starter-flow-spring/tree/partial-match

1. Open localhost:8080/
2. click on the router link to navigate to /second
3. see no content except main layout. 

Not happening if I rollback to `partialMatch=false`
created_at: 2025-03-31T13:02:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -267,6 +279,41 @@ private boolean populateChain(ArrayList<HasElement> chain,
                 return true;
             }
             chain.addAll(maybeChain.get());
+
+            // If partialMatch is set to true check if the cache contains a
+            // chain and possibly request extended details to get window name
+            // to select cached chain.
+            if (chain.isEmpty() && isPreservePartialTarget(
+                    navigationState.getNavigationTarget(), routeLayoutTypes)) {
+                UI ui = event.getUI();
+                if (ui.getInternals().getExtendedClientDetails() == null) {
+                    PreservedComponentCache cache = ui.getSession()
+                            .getAttribute(PreservedComponentCache.class);
+                    if (cache != null && !cache.isEmpty()) {
+                        // As there is a cached chain we get the client details
+                        // to get the window name so we can determine if the
+                        // cache contains a chain for us to use.
+                        ui.getPage().retrieveExtendedClientDetails(
+                                details -> handle(event));
+                        return true;
+                    }
+                } else {
+                    partialChain = getWindowPreservedChain(ui.getSession(),
+                            ui.getInternals().getExtendedClientDetails()
+                                    .getWindowName());
+                    if (partialChain != null) {
+                        disconnectElements(partialChain, ui);
+                        // Remove all router layout contents as parts are
+                        // reused and there is no old chain to remove against.
+                        for (int i = 0; i < partialChain.size() - 1; i++) {
+                            HasElement child = partialChain.get(i);
+                            RouterLayout parent = (RouterLayout) partialChain
+                                    .get(i + 1);
+                            parent.removeRouterLayoutContent(child);
+                        }
path: flow-server/src/main/java/com/vaadin/flow/router/internal/AbstractNavigationStateRenderer.java
user: login: mshabarov
body: Don't know if this a consequence of the previous anomaly, but if, after router link navigation to the second view, I refresh the browser, the following exception is thrown:

<details>
<summary>Error log</summary>

```
2025-03-31T15:52:30.294+03:00 ERROR 11782 --- [nio-8080-exec-1] c.v.flow.router.InternalServerError      : There was an exception while trying to navigate to 'second'
java.lang.IllegalStateException: Can't move a node from one state tree to another. If this is intentional, first remove the node from its current state tree by calling removeFromTree. This usually happens when a component is moved from one UI to another, which is not recommended. This may be caused by assigning components to static members or spring singleton scoped beans and referencing them from multiple UIs. Offending component: created: Component 'org.springframework.context.support.AbstractApplicationContext' at 'AbstractApplicationContext.java' (getBean LINE 1290), attached: Component 'com.vaadin.flow.router.RouterLayout' at 'RouterLayout.java' (showRouterLayoutContent LINE 49)
	at com.vaadin.flow.internal.StateNode.doSetTree(StateNode.java:801) ~[flow-server-24.8-SNAPSHOT.jar:24.8-SNAPSHOT]
	at com.vaadin.flow.internal.StateNode.lambda$setTree$3(StateNode.java:398) ~[flow-server-24.8-SNAPSHOT.jar:24.8-SNAPSHOT]
	at com.vaadin.flow.internal.StateNode.visitNodeTree(StateNode.java:737) ~[flow-server-24.8-SNAPSHOT.jar:24.8-SNAPSHOT]
	at com.vaadin.flow.internal.StateNode.setTree(StateNode.java:398) ~[flow-server-24.8-SNAPSHOT.jar:24.8-SNAPSHOT]
	at com.vaadin.flow.internal.StateNode.setParent(StateNode.java:281) ~[flow-server-24.8-SNAPSHOT.jar:24.8-SNAPSHOT]
	at com.vaadin.flow.internal.nodefeature.NodeFeature.attachPotentialChild(NodeFeature.java:80) ~[flow-server-24.8-SNAPSHOT.jar:24.8-SNAPSHOT]
	at com.vaadin.flow.internal.nodefeature.StateNodeNodeList.add(StateNodeNodeList.java:55) ~[flow-server-24.8-SNAPSHOT.jar:24.8-SNAPSHOT]
	at com.vaadin.flow.internal.nodefeature.ElementChildrenList.add(ElementChildrenList.java:44) ~[flow-server-24.8-SNAPSHOT.jar:24.8-SNAPSHOT]
	at com.vaadin.flow.dom.impl.AbstractNodeStateProvider.insertChild(AbstractNodeStateProvider.java:107) ~[flow-server-24.8-SNAPSHOT.jar:24.8-SNAPSHOT]
	at com.vaadin.flow.dom.Node.insertChild(Node.java:391) ~[flow-server-24.8-SNAPSHOT.jar:24.8-SNAPSHOT]
	at com.vaadin.flow.dom.Node.appendChild(Node.java:163) ~[flow-server-24.8-SNAPSHOT.jar:24.8-SNAPSHOT]
	at com.vaadin.flow.dom.Node.appendChild(Node.java:147) ~[flow-server-24.8-SNAPSHOT.jar:24.8-SNAPSHOT]
	at com.vaadin.flow.router.RouterLayout.showRouterLayoutContent(RouterLayout.java:49) ~[flow-server-24.8-SNAPSHOT.jar:24.8-SNAPSHOT]
	at com.vaadin.flow.component.internal.UIInternals.showRouteTarget(UIInternals.java:884) ~[flow-server-24.8-SNAPSHOT.jar:24.8-SNAPSHOT]
	at com.vaadin.flow.router.internal.AbstractNavigationStateRenderer.handle(AbstractNavigationStateRenderer.java:241) ~[flow-server-24.8-SNAPSHOT.jar:24.8-SNAPSHOT]
	at com.vaadin.flow.component.internal.JavaScriptNavigationStateRenderer.handle(JavaScriptNavigationStateRenderer.java:78) ~[flow-server-24.8-SNAPSHOT.jar:24.8-SNAPSHOT]
	at com.vaadin.flow.component.UI.handleNavigation(UI.java:2057) ~[flow-server-24.8-SNAPSHOT.jar:24.8-SNAPSHOT]
	at com.vaadin.flow.component.UI.renderViewForRoute(UI.java:2019) ~[flow-server-24.8-SNAPSHOT.jar:24.8-SNAPSHOT]
	at com.vaadin.flow.component.UI.lambda$browserNavigate$3bdbbbb5$1(UI.java:1878) ~[flow-server-24.8-SNAPSHOT.jar:24.8-SNAPSHOT]
	at com.vaadin.flow.component.UI.browserNavigate(UI.java:1889) ~[flow-server-24.8-SNAPSHOT.jar:24.8-SNAPSHOT]
	at com.vaadin.flow.component.ComponentEventBus.fireEventForListener(ComponentEventBus.java:244) ~[flow-server-24.8-SNAPSHOT.jar:24.8-SNAPSHOT]
	at com.vaadin.flow.component.ComponentEventBus.handleDomEvent(ComponentEventBus.java:501) ~[flow-server-24.8-SNAPSHOT.jar:24.8-SNAPSHOT]
	at com.vaadin.flow.component.ComponentEventBus.lambda$addDomTrigger$dd1b7957$1(ComponentEventBus.java:303) ~[flow-server-24.8-SNAPSHOT.jar:24.8-SNAPSHOT]
	at com.vaadin.flow.internal.nodefeature.ElementListenerMap.lambda$fireEvent$2(ElementListenerMap.java:475) ~[flow-server-24.8-SNAPSHOT.jar:24.8-SNAPSHOT]
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1597) ~[na:na]
	at com.vaadin.flow.internal.nodefeature.ElementListenerMap.fireEvent(ElementListenerMap.java:475) ~[flow-server-24.8-SNAPSHOT.jar:24.8-SNAPSHOT]
	at com.vaadin.flow.server.communication.rpc.EventRpcHandler.handleNode(EventRpcHandler.java:62) ~[flow-server-24.8-SNAPSHOT.jar:24.8-SNAPSHOT]
	at com.vaadin.flow.server.communication.rpc.AbstractRpcInvocationHandler.handle(AbstractRpcInvocationHandler.java:79) ~[flow-server-24.8-SNAPSHOT.jar:24.8-SNAPSHOT]
	at com.vaadin.flow.server.communication.ServerRpcHandler.handleInvocationData(ServerRpcHandler.java:568) ~[flow-server-24.8-SNAPSHOT.jar:24.8-SNAPSHOT]
	at com.vaadin.flow.server.communication.ServerRpcHandler.lambda$handleInvocations$6(ServerRpcHandler.java:549) ~[flow-server-24.8-SNAPSHOT.jar:24.8-SNAPSHOT]
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1597) ~[na:na]
	at com.vaadin.flow.server.communication.ServerRpcHandler.handleInvocations(ServerRpcHandler.java:549) ~[flow-server-24.8-SNAPSHOT.jar:24.8-SNAPSHOT]
	at com.vaadin.flow.server.communication.ServerRpcHandler.handleRpc(ServerRpcHandler.java:376) ~[flow-server-24.8-SNAPSHOT.jar:24.8-SNAPSHOT]
	at com.vaadin.flow.server.communication.UidlRequestHandler.synchronizedHandleRequest(UidlRequestHandler.java:136) ~[flow-server-24.8-SNAPSHOT.jar:24.8-SNAPSHOT]
	at com.vaadin.flow.server.SynchronizedRequestHandler.handleRequest(SynchronizedRequestHandler.java:63) ~[flow-server-24.8-SNAPSHOT.jar:24.8-SNAPSHOT]
	at com.vaadin.flow.server.VaadinService.handleRequest(VaadinService.java:1661) ~[flow-server-24.8-SNAPSHOT.jar:24.8-SNAPSHOT]
	at com.vaadin.flow.server.VaadinServlet.service(VaadinServlet.java:398) ~[flow-server-24.8-SNAPSHOT.jar:24.8-SNAPSHOT]
	at com.vaadin.flow.spring.SpringServlet.service(SpringServlet.java:106) ~[vaadin-spring-24.8-SNAPSHOT.jar:na]
	at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658) ~[tomcat-embed-core-10.1.36.jar:6.0]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:195) ~[tomcat-embed-core-10.1.36.jar:10.1.36]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.36.jar:10.1.36]
	at org.apache.catalina.core.ApplicationDispatcher.invoke(ApplicationDispatcher.java:633) ~[tomcat-embed-core-10.1.36.jar:10.1.36]
	at org.apache.catalina.core.ApplicationDispatcher.processRequest(ApplicationDispatcher.java:409) ~[tomcat-embed-core-10.1.36.jar:10.1.36]
	at org.apache.catalina.core.ApplicationDispatcher.doForward(ApplicationDispatcher.java:304) ~[tomcat-embed-core-10.1.36.jar:10.1.36]
	at org.apache.catalina.core.ApplicationDispatcher.forward(ApplicationDispatcher.java:268) ~[tomcat-embed-core-10.1.36.jar:10.1.36]
	at org.springframework.web.servlet.mvc.ServletForwardingController.handleRequestInternal(ServletForwardingController.java:142) ~[spring-webmvc-6.2.3.jar:6.2.3]
	at org.springframework.web.servlet.mvc.AbstractController.handleRequest(AbstractController.java:178) ~[spring-webmvc-6.2.3.jar:6.2.3]
	at org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter.handle(SimpleControllerHandlerAdapter.java:51) ~[spring-webmvc-6.2.3.jar:6.2.3]
	at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1088) ~[spring-webmvc-6.2.3.jar:6.2.3]
	at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:978) ~[spring-webmvc-6.2.3.jar:6.2.3]
	at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1014) ~[spring-webmvc-6.2.3.jar:6.2.3]
	at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:914) ~[spring-webmvc-6.2.3.jar:6.2.3]
	at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:590) ~[tomcat-embed-core-10.1.36.jar:6.0]
	at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:885) ~[spring-webmvc-6.2.3.jar:6.2.3]
	at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658) ~[tomcat-embed-core-10.1.36.jar:6.0]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:195) ~[tomcat-embed-core-10.1.36.jar:10.1.36]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.36.jar:10.1.36]
	at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51) ~[tomcat-embed-websocket-10.1.36.jar:10.1.36]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.36.jar:10.1.36]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.36.jar:10.1.36]
	at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-6.2.3.jar:6.2.3]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.2.3.jar:6.2.3]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.36.jar:10.1.36]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.36.jar:10.1.36]
	at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-6.2.3.jar:6.2.3]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.2.3.jar:6.2.3]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.36.jar:10.1.36]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.36.jar:10.1.36]
	at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-6.2.3.jar:6.2.3]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.2.3.jar:6.2.3]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.36.jar:10.1.36]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.36.jar:10.1.36]
	at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167) ~[tomcat-embed-core-10.1.36.jar:10.1.36]
	at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90) ~[tomcat-embed-core-10.1.36.jar:10.1.36]
	at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:483) ~[tomcat-embed-core-10.1.36.jar:10.1.36]
	at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:115) ~[tomcat-embed-core-10.1.36.jar:10.1.36]
	at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93) ~[tomcat-embed-core-10.1.36.jar:10.1.36]
	at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74) ~[tomcat-embed-core-10.1.36.jar:10.1.36]
	at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:344) ~[tomcat-embed-core-10.1.36.jar:10.1.36]
	at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:397) ~[tomcat-embed-core-10.1.36.jar:10.1.36]
	at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63) ~[tomcat-embed-core-10.1.36.jar:10.1.36]
	at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:905) ~[tomcat-embed-core-10.1.36.jar:10.1.36]
	at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1743) ~[tomcat-embed-core-10.1.36.jar:10.1.36]
	at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52) ~[tomcat-embed-core-10.1.36.jar:10.1.36]
	at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1190) ~[tomcat-embed-core-10.1.36.jar:10.1.36]
	at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) ~[tomcat-embed-core-10.1.36.jar:10.1.36]
	at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:63) ~[tomcat-embed-core-10.1.36.jar:10.1.36]
```

</details>

created_at: 2025-03-31T13:07:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -45,4 +45,14 @@
 @Inherited
 @Documented
 public @interface PreserveOnRefresh {
+
+    /**
+     * Set to true if refresh should also reuse partial chain components of
+     * stored view chain.
path: flow-server/src/main/java/com/vaadin/flow/router/PreserveOnRefresh.java
user: login: caalador
body: Added more information.
created_at: 2025-04-01T09:45:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -267,6 +279,41 @@ private boolean populateChain(ArrayList<HasElement> chain,
                 return true;
             }
             chain.addAll(maybeChain.get());
+
+            // If partialMatch is set to true check if the cache contains a
+            // chain and possibly request extended details to get window name
+            // to select cached chain.
+            if (chain.isEmpty() && isPreservePartialTarget(
+                    navigationState.getNavigationTarget(), routeLayoutTypes)) {
+                UI ui = event.getUI();
+                if (ui.getInternals().getExtendedClientDetails() == null) {
+                    PreservedComponentCache cache = ui.getSession()
+                            .getAttribute(PreservedComponentCache.class);
+                    if (cache != null && !cache.isEmpty()) {
+                        // As there is a cached chain we get the client details
+                        // to get the window name so we can determine if the
+                        // cache contains a chain for us to use.
+                        ui.getPage().retrieveExtendedClientDetails(
+                                details -> handle(event));
+                        return true;
+                    }
+                } else {
+                    partialChain = getWindowPreservedChain(ui.getSession(),
+                            ui.getInternals().getExtendedClientDetails()
+                                    .getWindowName());
+                    if (partialChain != null) {
+                        disconnectElements(partialChain, ui);
+                        // Remove all router layout contents as parts are
+                        // reused and there is no old chain to remove against.
+                        for (int i = 0; i < partialChain.size() - 1; i++) {
+                            HasElement child = partialChain.get(i);
+                            RouterLayout parent = (RouterLayout) partialChain
+                                    .get(i + 1);
+                            parent.removeRouterLayoutContent(child);
+                        }
path: flow-server/src/main/java/com/vaadin/flow/router/internal/AbstractNavigationStateRenderer.java
user: login: caalador
body: Updated cleanup to clean unwanted part
created_at: 2025-04-01T09:46:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,33 @@
+/*
+ * Copyright 2000-2025 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.vaadin.flow.misc.ui.partial;
+
+import com.vaadin.flow.component.html.Div;
+import com.vaadin.flow.router.PreserveOnRefresh;
+import com.vaadin.flow.router.Route;
+import com.vaadin.flow.router.RouterLink;
+
+@Route(value = "main", layout = MainLayout.class)
+@PreserveOnRefresh
path: flow-tests/test-misc/src/main/java/com/vaadin/flow/misc/ui/partial/MainView.java
user: login: caalador
body: removed
created_at: 2025-04-01T09:46:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1110,6 +1173,26 @@ static Optional<ArrayList<HasElement>> getPreservedChain(
         }
     }
 
+    /**
+     * Get a preserved chain by window name only ignoring location path.
+     *
+     * @param session
+     *            current session
+     * @param windowName
+     *            window name to get cached view stack for
+     * @return view stack cache if available for window name
+     */
+    static List<HasElement> getWindowPreservedChain(VaadinSession session,
+            String windowName) {
+        final PreservedComponentCache cache = session
+                .getAttribute(PreservedComponentCache.class);
+        if (cache != null && cache.containsKey(windowName)) {
+            return cache.get(windowName).getSecond();
+        } else {
+            return null;
path: flow-server/src/main/java/com/vaadin/flow/router/internal/AbstractNavigationStateRenderer.java
user: login: caalador
body: Changed as usage changed.
created_at: 2025-04-01T09:46:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -807,6 +807,18 @@ public boolean cancelPendingTitleUpdate() {
         return result;
     }
 
+    /**
+     * Populate the routerTargetChain with RouterLayouts, but only if the target
+     * chain is empty. If the chain contains elements the given list is ignored.
+     *
+     * @param layouts
path: flow-server/src/main/java/com/vaadin/flow/component/internal/UIInternals.java
user: login: mshabarov
body: ```suggestion
     * @param layouts router layouts to be shown after navigation
```
created_at: 2025-04-02T05:33:40Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-03-27T10:59:02Z
body: ## Test Results
1 181 files  + 45  1 181 suites  +45   1h 13m 20s ⏱️ - 2m 48s
8 113 tests +307  8 056 ✅ +308  57 💤 ±0  0 ❌  - 1 
8 445 runs  +274  8 379 ✅ +275  66 💤 ±0  0 ❌  - 1 

Results for commit 3efdb2f7. ± Comparison against base commit 05a3ac4b.

[test-results]:data:application/gzip;base64,H4sIAKgI7mcC/03MSw6DIBSF4a0Yxh1A4fLoZgzySEhVGoRR070XrVKG57s3/xv5MLsNPQZCJLkNaCsh99uWpHOIaxXGMK5Sz3l/kITQa45bMWY3DPxvz/CqBqKJ12Gu0iqjSymmU1JZjyxjcK5WpUI1+kU5v6BrHrtPmrgsIdeBqPN2unvBDQepBYZJSunBaOGpNqAtYKoEVejzBbQ9fCsSAQAA

:recycle: This comment has been updated with latest results.
author_association: NONE
user: login: caalador
created_at: 2025-04-02T04:50:56Z
body: Fixed. 
Added the test even though the feature doesn't change the normal functionality of navigation chain reuse.
author_association: CONTRIBUTOR
user: login: mshabarov
created_at: 2025-04-02T05:36:28Z
body: New tests fail with unexpected router layout count, maybe the test needs more clean up or counter reset?
author_association: CONTRIBUTOR
user: login: caalador
created_at: 2025-04-02T05:55:24Z
body: Dropped the second test at they seem to run on each other generating really strange results that do not happen manually.
author_association: CONTRIBUTOR
user: login: sonarqubecloud[bot]
created_at: 2025-04-03T03:53:46Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=vaadin_flow&pullRequest=21189) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [1774 New issues](https://sonarcloud.io/project/issues?id=vaadin_flow&pullRequest=21189&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [9 Accepted issues](https://sonarcloud.io/project/issues?id=vaadin_flow&pullRequest=21189&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=vaadin_flow&pullRequest=21189&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=vaadin_flow&pullRequest=21189&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=vaadin_flow&pullRequest=21189&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=vaadin_flow&pullRequest=21189)


author_association: NONE
user: login: vaadin-bot
created_at: 2025-06-02T10:14:01Z
body: This ticket/PR has been released with Vaadin 24.8.0.beta1 and is also targeting the upcoming stable 24.8.0 version.
author_association: COLLABORATOR