Vaadin 8→24 Migration: ComboBox Flickering with Lazy Loading

Hey There,

Our company is currently migrating from Vaadin 8 to Vaadin 24. In Vaadin 8, we extensively used Max Schuster’s AutoComplete extension for TextFields, which provided excellent lazy loading capabilities with smooth, flicker-free transitions when fetching data from the server.

With Vaadin 24’s native ComboBox component, we’re experiencing a flickering issue when implementing lazy loading for autocomplete functionality. The overlay briefly closes and reopens when new data is fetched.

The ComboBox examples on Vaadin’s documentation page (Combo Box component | Vaadin components) don’t show this flickering. My guess is they filter data in memory rather than implementing true lazy loading from a server/database. Or did I missunderstand something.

Has anyone successfully eliminated ComboBox flickering with server-side lazy loading in Vaadin 24? Looking for ways to override the internal behavior or alternative implementations that maintain smooth UX during data updates.

Many Thanks!
Matti

One thing to note is that the component examples in the docs always render the React example. To see the rendered Flow example you have to make sure you’re on the Flow tab, and then open the example in a new tab. For example:

I can not observe the effect you described with those, the overlay stays open after changing the filter.

I would suggest to test how the component behaves when set up like in the examples. Apart from that it would help if you could show some code on how your combo box is set up, which could help others with potentially identifying your issue. Would also be useful to have the specific Vaadin version you’re using.

Hi Sascha,

Thank you for your help! I checked the documentation example again. The Lit and React examples do not show the spinner and flickering. The transition is very smooth. However, when I open the Flow example in a new tab, I do indeed see the flickering and spinner. I tried this with different browsers (Safari, Chrome, Firefox) with the same result. We are using Vaadin 24.8.4.

Below you will find the code for my AutoCompleteField.

package zetcom.group.base.ui.component;

import com.vaadin.flow.component.combobox.ComboBox;
import com.vaadin.flow.component.dependency.CssImport;
import com.vaadin.flow.data.provider.DataProvider;
import com.vaadin.flow.data.provider.Query;

import java.io.Serial;
import java.util.List;
import java.util.Locale;
import java.util.function.Supplier;
import java.util.logging.Logger;
import java.util.stream.Stream;

@CssImport("themes/default/components/vaadin-combo-box.css")
public class AutoCompleteField extends ComboBox<String> {

    Logger LOG = Logger.getLogger(AutoCompleteField.class.getName());

    private final int minChars;
    private final Supplier<List<String>> dataSupplier;


    public AutoCompleteField(Supplier<List<String>> dataSupplier) {
        this.minChars = 3;
        this.dataSupplier = dataSupplier;

        setAllowCustomValue(true);
        setClearButtonVisible(false);
        setPageSize(50);
        setAutoOpen(false);

        setPlaceholder("Type at least " + this.minChars + " characters");

        // DataProvider with server-side filtering and paging
        DataProvider<String, String> dataProvider = DataProvider.fromFilteringCallbacks(
                this::fetchItems,
                this::countItems
        );

        setItems(dataProvider);

        getElement().addPropertyChangeListener("filter",event -> {
            LOG.info("filter-changed: filter=" + event.getValue());
            int len = event.getValue().toString().trim().length();
            if (len < this.minChars) {
                // Prevent opening
                setOpened(false);
                if (isAutoOpen()) {
                    setAutoOpen(false);
                }
            } else {
                // Enable auto-open and open once the filter is synced
                if (!isAutoOpen()) {
                    setAutoOpen(true);
                }
                getUI().ifPresent(ui -> ui.beforeClientResponse(this, ctx -> setOpened(true)));
            }
        });

        interceptEscapeKey();

    }

    private void interceptEscapeKey() {
        addAttachListener(e -> {
            getElement().executeJs("""
                    const inputElement = this.inputElement || this.$.input;
                    if (inputElement) {
                        inputElement.addEventListener('keydown', (event) => {
                            if (event.key === 'Escape' && this.opened) {
                                event.stopPropagation();
                                event.preventDefault();
                                this.opened = false;
                                this.autoOpenDisabled = false;
                            }
                        });
                    }
                    """);
        });
    }

    private Stream<String> fetchItems(Query<String, String> query) {
        String filter = query.getFilter().orElse("").trim().toLowerCase(Locale.ROOT);

        if (filter.length() < this.minChars) {
            return Stream.empty();
        }

        LOG.info("Fetching items for filter: '" + filter + "'");

        return dataSupplier.get().stream()
                .filter(s -> s.toLowerCase(Locale.ROOT).contains(filter))
                .skip(query.getOffset())
                .limit(query.getLimit());
    }


    private int countItems(Query<String, String> query) {
        String filter = query.getFilter().orElse("").trim().toLowerCase(Locale.ROOT);

        if (filter.length() < this.minChars) {
            return 0;
        }

        LOG.info("Counting items for filter: '" + filter + "'");

        return (int) dataSupplier.get().stream()
                .filter(s -> s.toLowerCase(Locale.ROOT).contains(filter))
                .count();
    }

}


If by flickering you mean that after changing the filter the items in the dropdown temporarily reduce to a single placeholder item that shows a loading indicator, that is indeed intentional. While this happens quite fast, it should still be in the range of several hundred milliseconds. You also mentioned that the overlay closes in your OP which should not be the case?

This is how it looks for me in Firefox:

To my knowledge there is no way around that when using lazy loading / callbacks.

In Chrome there is a small glitch where the loading spinner animation resets when the filter changes.

Hi Sascha, sorry for the late reply, I was on vacation. My sentence “It closes” was not accurate. I meant the recalculation of the suggested terms. This is what Max Schuster’s Autocomplete Extension for TextFields in Vaadin 8 handled much better: a smooth transition between two selections.
But anyway, many thanks for your help!