Skip to content

feat: retain local signal values during hotswap#23854

Merged
mshabarov merged 10 commits intomainfrom
feat/23830-retain-signal-value-on-hotswap
Mar 16, 2026
Merged

feat: retain local signal values during hotswap#23854
mshabarov merged 10 commits intomainfrom
feat/23830-retain-signal-value-on-hotswap

Conversation

@tltv
Copy link
Member

@tltv tltv commented Mar 12, 2026

Automatically transfer local signal values from old view fields to matching fields in the new view during hotswap refresh.

Fixes: #23830

This pull request introduces a new mechanism for transferring local signal field values between view instances during a hot-reload (hotswap refresh) in development mode. The main addition is the SignalFieldTransfer utility, which ensures that the state held in ValueSignal and ListSignal fields is preserved when a view is refreshed, improving the developer experience.

The most important changes are:

Signal transfer utility

  • Added a new class SignalFieldTransfer in flow-server/src/main/java/com/vaadin/flow/internal/SignalFieldTransfer.java that provides static methods to transfer local signal field values (of type ValueSignal and ListSignal) from an old view instance to a new one, matching fields by name and type. This is used to preserve signal state during hot-reload in development.

Integration with navigation

  • Updated AbstractNavigationStateRenderer#getRouteTarget to call SignalFieldTransfer.transferLocalSignalValues when a view is refreshed (triggered by NavigationTrigger.REFRESH_ROUTE) and the application is not in production mode, ensuring signal state is transferred during development hot-reloads.
  • Imported the new SignalFieldTransfer class in AbstractNavigationStateRenderer.java to enable the above integration.

Automatically transfer local signal values from old view fields to matching fields in the new view during hotswap refresh.

Fixes: #23830
@github-actions github-actions bot added +0.1.0 and removed +0.0.1 labels Mar 12, 2026
@github-actions
Copy link

github-actions bot commented Mar 12, 2026

Test Results

 1 386 files  + 59   1 386 suites  +59   1h 27m 56s ⏱️ - 4m 56s
 9 922 tests +676   9 851 ✅ +729  71 💤 ±0  0 ❌  - 10 
10 395 runs  +676  10 315 ✅ +731  80 💤 ±0  0 ❌  - 10 

Results for commit ee9747b. ± Comparison against base commit 50aba91.

♻️ This comment has been updated with latest results.

@tltv tltv marked this pull request as ready for review March 13, 2026 11:31
Copy link
Member

@Legioth Legioth left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will also look at the tests but posting findings from reviewing the implementation right away

}

private static boolean isLocalSignalType(Class<?> type) {
return ValueSignal.class.isAssignableFrom(type)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe check for AbstractLocalSignal instead so that any future subclasses are also automatically covered?


private static void transferField(HasElement oldInstance,
HasElement newInstance, Field newField) throws Exception {
Field oldField = findField(oldInstance.getClass(), newField.getName());
Copy link
Member

@Legioth Legioth Mar 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking through the entire type hierarchy might lead to problems in the edge case case when multiple classes in the hierarchy have a private field with the same name. Would it work to also pass in newClass from the transferLocalSignalValues method and somehow use that either for the lookup or for additional verification?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a bit more complicated even, because we can't mix old and new class to read fields since class may have changed. Need to iterate both class hierarchies in parallel instead.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about matching fields only if the fully qualified class names match?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's another option. Both probably works, but this might be more efficient even.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Efficiency is relatively irrelevant considering all the other processing that happens in a hotswap scenario. I'm mostly thinking of correctness to avoid accidentally mixing up signal values.

if (oldField == null) {
return;
}
if (!isLocalSignalType(oldField.getType())) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could check that it's the same type and not just any local signal type. That would also allow slightly simplifying things further down.

? Optional.empty()
: findActiveRouteTarget(event, isRouteTargetType);
return (T) currentInstance.orElseGet(
T result = (T) currentInstance.orElseGet(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could use a more descriptive variable name, e.g. routeTarget

var newView = new ViewWithListSignal();
SignalFieldTransfer.transferLocalSignalValues(oldView, newView);

var values = newView.items.peek().stream().map(ValueSignal::peek)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be simplified to newView.items.peekValues().toList()

}

@Test
void exceptionFromSignalSetIsLogged() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Misleading method name since it doesn't assert anything about logging

@tltv tltv requested a review from Legioth March 16, 2026 10:45
Legioth
Legioth previously approved these changes Mar 16, 2026
Copy link
Member

@Legioth Legioth left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One minimal stylistic remark. Feel free to merge without adjusting or to adjust and then merge right away without waiting for a follow-up review.

Class<?> current = targetClazz;
while (current != null && current != Object.class) {
try {
if (Objects.equals(current.getName(),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused by the use of Objects.equals here since it implies that either value might be null but I don't see how that could ever happen. (And if still can happen, then I don't think we should match in that case)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed unnecessary usage of Object.equals.

@sonarqubecloud
Copy link

@mshabarov mshabarov merged commit 09d7e0c into main Mar 16, 2026
51 of 52 checks passed
@mshabarov mshabarov deleted the feat/23830-retain-signal-value-on-hotswap branch March 16, 2026 12:04
vaadin-bot pushed a commit that referenced this pull request Mar 16, 2026
* feat: retain local signal values during hotswap

Automatically transfer local signal values from old view fields to matching fields in the new view during hotswap refresh.

Fixes: #23830

* make SignalFieldTransfer Serializable

* update javadoc

* Add more tests

* Add unit test and remove unused constructor

* Fix code review findings

* remove unnecessary java.util.Objects usage

---------

Co-authored-by: Leif Åstrand <leif@vaadin.com>
mshabarov pushed a commit that referenced this pull request Mar 16, 2026
* feat: retain local signal values during hotswap

Automatically transfer local signal values from old view fields to matching fields in the new view during hotswap refresh.

Fixes: #23830

* make SignalFieldTransfer Serializable

* update javadoc

* Add more tests

* Add unit test and remove unused constructor

* Fix code review findings

* remove unnecessary java.util.Objects usage

---------

Co-authored-by: Tomi Virtanen <tltv@vaadin.com>
Co-authored-by: Leif Åstrand <leif@vaadin.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Retain signal values when hotswapping views

4 participants