Environment
- Jmix 3.0.3 (
io.jmix.tabbedmode:jmix-tabbedmode-flowui:3.0.3) - Vaadin 25.1.14, Spring Boot 4, Java 21
- Aura theme, Chrome (latest)
Description
In tabbed mode, if the selected tab changes twice before the server has answered the first change, the tab sheet
gets stuck in an endless client/server loop. The browser keeps switching between the two tabs, sending ~3 UIDL
requests per second. The loading indicator never disappears until the tab (or the page) is closed.
It is easy to trigger by clicking two tabs in quick succession. It also happens with normal clicks when the view in
the selected tab is slow to re-render, because that makes the round trip longer. For example, a view with a large
DataGrid that uses component renderers re-sends its rows when its tab is shown again.
It is not related to our views: we reproduce it with any two tabs, e.g. the home view and a simple list view.
Steps to reproduce
- Open an application in tabbed mode and open two views in two tabs (A and B).
- Select tab A and then tab B within one server round trip. For example, run this in the browser console:
const tabs = [...document.querySelectorAll('jmix-view-tab')]; tabs[0].click(); setTimeout(() => tabs[1].click(), 30); - Watch the network tab or the loading indicator.
Expected: tab B stays selected and the requests stop.
Actual: the selection keeps toggling between A and B forever (3 UIDL requests per second), and the loading
indicator stays visible.
Analysis
MainTabSheet.initComponent() sets the element property selected on every selection change of the inner Tabs,
including changes that come from the client:
addSelectedChangeListener(e -> {
getElement().setProperty("selected", tabs.getSelectedIndex());
updateContent();
});
The client has already updated jmix-main-tabsheet.selected itself (TabsSlotController.__tabsSelectedChangedListener).
The server then sends the same value back. When a second change is already in flight, that echo is stale.
Captured sequence (tabs = vaadin-tabs node, sheet = jmix-main-tabsheet node):
553ms sheet=0 (prev 1) <- setJsProperty // stale echo from the server response
556ms SEND tabs.selected=1 // pending client change
558ms tabs=0 (prev 1) <- JmixMainTabSheet._delegateProperty
639ms sheet=1 (prev 0) <- setJsProperty // stale echo again
647ms SEND tabs.selected=0
649ms tabs=1 (prev 0) <- JmixMainTabSheet._delegateProperty
...
Each response applies the previous selection to sheet.selected. The tab sheet delegates it to vaadin-tabs, which
fires selected-changed and sends a new change, and so on. The two values stay one round trip out of phase and
the loop never ends.
Workaround
We replaced MainTabSheet with a subclass that does not echo client-originated changes. It only updates the
server-side property model, without emitting a change to the client:
public class FsipMainTabSheet extends MainTabSheet {
@Override
protected void initComponent() {
tabs = createTabsComponent();
SlotUtils.addToSlot(this, "tabs", tabs);
addSelectedChangeListener(e -> {
Double selected = (double) tabs.getSelectedIndex();
if (e.isFromClient()) {
// the client already has this value: keep the server model in sync without sending it back
getElement().getNode().getFeature(ElementPropertyMap.class)
.setProperty("selected", selected, false);
} else {
getElement().setProperty("selected", selected);
}
updateContent();
});
addListener(MainTabSheetDropEvent.class, this::onDrop);
}
}
It is registered with:
ComponentRegistrationBuilder.create(FsipMainTabSheet.class)
.replaceComponent(MainTabSheet.class)
.build();
Server-initiated selections (opening or closing views, activateSelectedTab()) keep the original behaviour.
Suggested fix
In MainTabSheet.initComponent(), do not send the selected property back to the client when
SelectedChangeEvent.isFromClient() is true. The workaround above uses ElementPropertyMap.setProperty(name, value, false). Alternatively, declare selected as a client-synchronized property of jmix-main-tabsheet.