Skip to content

Production-ready Signals API #8366

@mshabarov

Description

@mshabarov

Description

Complete the Signals API and make it production-ready way of building reactive UI with Java and Vaadin by:

  • providing a high-level API in Components and core for common UI building cases and support specific and less often cases with the low-level API and helper classes,
  • updating Vaadin online documentation to use Signals in "Building Apps" articles at the first place and having components articles updated with the new methods, but still referencing the old way of managing components state.
  • updating start.vaadin.com, all the starter projects to use Signals.

Tier

Free

License

Apache 2.0

Motivation

Background

Vaadin 24.8, 24.9 have shipped the base Signals class hierarchy and helpers in ComponentEffect.
Vaadin 25.0 has added Element-level binding methods and helpers in ElementEffect.

Vaadin 25.1 now aims to fully integrate Signal API into Vaadin Flow Components, complete the core API, make Signal classes serializable and conclude the overall design for the reactive UI state management, so that developers can use this way of building UI as a primary.

Problem

Overall problem: It is common in Vaadin to explicitly handle UI state change by users in the event listeners, maintain clean ups and set the UI state explicitly through the component's methods. This may lead to a boiler-plate code that makes the view classes be unreadable and hard-maintained. Signals on the other hand may simplify the UI development by implicitly handling the state change no matter what kind of source did this - single user event, multi-user events or asynchronous background event, and implicitly handling the component UI attached/detached state.

Another frequent pattern is to make components be the Java class fields in "view" Java classes, where they are accessed along the class in different places, e.g. from the listeners of another component, form binding codes or even from outer layout Java class. Signals can help separating the "state" from the components, leaving only static initialization (e.g. constant column settings) from the dynamically changed content. This may relief the need of component class fields, but making them state fields instead, whereas the component declaration can be local method-scoped.

The need of using UI.access() may bloat the code and requires a knowledge that this method exists and how to use it. Signals incapsulate this detail and basically use it under the hood, so the client code can just update the signal value and async updates work (of course the app needs to enable Push async updates).

Finally, whereas the Signal and related classes are available, it's not yet possible to code the reactive UIs using signals and without using existing low-level Element API, explicit effect functions and defining addValueChangeListener(), nor there is a way of binding signals to a form.

Solution

Signals integration to Vaadin Components would include:

  • Signal binding methods to Component class and mix-in interfaces for commonly used component properties (visible, enabled, value)
  • Component-specific signal binding methods (e.g. setHelperText)
  • Two-way signal binding for standalone field components and forms.
  • Ensure Signal API is serializable.

This in the end means the component provide bind methods for the component features for the following kind of bindings

  • one way binding (e.g. text)
  • two-way binding (e.g. value)
  • callbacks that use signal as a state (e.g. item label generator),
    and the Binder API is adjusted/extended so it's features can use Signal and benefit from two-way bindings.

As an example of how the code may look like when using signals, guess the products price calculator add VAT to the base price and updates all product cards:

Example with the value change listeners:

NumberField vatField = new NumberField("VAT %");
// 1. Value change listener explicitly added
Registration vatChangeRegistration = vatField.addValueChangeListener(event -> {
    productCards.forEach(productCard -> {
           String newPriceLine = getUpdatedPrice(event.getValue());
           // 2. Product component explicitly updated with the new price
           productCard.setPriceLine(newPriceLine);
    });
});

// 3. Duplicated code that repeats the listener code for component initialization
productCards.forEach(productCard -> {
      // initialize the UI nearly the same way
});

// 4. Manual cleanup on detaching
vatChangeRegistration.remove();

Same example with signals:

// 3. State initialized once a signal is instantiated
ReferenceSignal<Double> vatSignal = new ReferenceSignal<>(24.0);

// 1. Two-way binding, no explicit listeners
// 4. No registration, signal is deactivated on detach
vatField.bindValue(vatSignal);

// Computed signal
Signal<String> priceWithVatText = vatSignal.map(this::getUpdatedPrice);

// 2. Bind computed signal to the product 
productCards.forEach(productCard -> productCard.bindText(priceWithVatText));

Requirements

  • Common signal binding methods for components:

    • Component and Element classes shall allow visibility binding to a boolean signal by using void bindVisible(Signal<Boolean> visibleSignal) new methods. Component delegates to Element and has the same contract
    • HasEnabled mix-in and Element class shall allow enabling/disabling them with a boolean signal by using void bindEnabled(Signal<Boolean> enabledSignal) new methods. HasEnabled delegates to Element and has the same contract
    • Components implementing HasText mix-in interface shall allow binding their text to a signal by using void bindText(Signal<String> textSignal) new method. This should delegate to the same existing method in Element
    • Vaadin Flow components (including Flow basic HTML components) shall allow passing the string signal to the constructor to establish the one-way text binding to the signal value, where a text value is already supported by exiting constructors, e.g. new Span(textSignal)
    • Components implementing HasValue<T> mix-in interface and Element shall allow a signal binding to their value by using void bindValue(WritableSignal<T> valueSignal) new methods. Note that this is a two-way binding that also updates the signal value when a ValueChangeEvent is fired.
  • Component-specific signal bindings:

    In addition to the above common cases in components, there are plenty of other more specific methods that components share thanks to mix-in interfaces or in the standalone flow component classes. For those cases the components shall allow the signal binding through the newly added methods:

    • One-way binding:
      A regular one-way bindings for a commonly updated configurations, that is not covered by text, enabled and visibility bindings, some examples: HasTheme::bindThemeName(String, Signal<Boolean>), HasHelper::bindHelperText(Signal<String>), CheckboxGroup::bindRequired(Signal<Boolean>).

      See the full list for all one-way binding cases.

    • Two-way binding:
      Two-way bindings are most common for setValue for components that implement HasValue (see next section for HasValue). The pattern is also applicable for some other cases that can be directly updated by the user such as whether a Details panel is opened, some examples: Details::setOpened(boolean), AppLayout::setDrawerOpened(boolean), Checkbox::setIndeterminate(boolean), MenuItemBase::setChecked(boolean), Crud::setDirty(boolean).

      See the full list for all two-way binding cases.

    • Other binding cases:
      Other binding methods for component-specific state less common than visible, enable, text and value, e.g. UI::setLocale(Locale), new AvatarGroup(AvatarGroupItem[]), new VerticalLayout(Component[]) etc. See the sub-issue for the full list.

  • Form binding and HasValue:

    Small adjustments should be done to the current Binder and HasValue APIs, that would make the most common cases possible for reactive form binding and input fields binding with signals. Designing a new signal-native form binding solution to replace Binder is out of scope of this PRD, if even decided.

    • Signal with the bean/record to edit:
      It should be possible to bind a bean signal to the form with a help of ComponentEffect and this should be documented, no new API is added:

      ComponentEffect.effect(this, () -> binder.readBean(beanToEditSignal.value());
      saveButton.addClickListener(event -> {
          Person person = new Person();
          binder.writeBean(person);
          service.save(person);
          beanToEditSignal.value(person);
      });
    • Cross-field validation:
      Field-level validator callbacks should be run inside a signal effect so that changes to any signal used by the validator will be detected and trigger running the validator again. Practical cross-field validation furthermore requires that the value of other bindings are available as signals, by adding a signal-aware value getter to Binding:

      var passwordBinding = binder.bind(passwordField, "password");
      
      binder.forField(confirmPasswordField)
          .withValidator(value -> !useStrictValidationSignal.value() || 
               Objects.equals(value, passwordBinding.value()), 
              "Must match the password")
          .bind("confirmPassword");
    • Reacting to validation status changes:
      Binder should provide a readonly signal that contains the latest BinderValidationStatus state. The signal value should be updated whenever a BinderValidationStatusHandler would have been notified. This should work the same regardless of whether a status handler is explicitly configured or if the default that delegates to handleBinderValidationStatus is in use.

      Binder<MyBean> binder = ...;
      Signal<BinderValidationStatus<MyBean>> validationStatusSignal = binder.getValidationStatus();
      submitButton.bindEnabled(() -> validationStatusSignal.value().isOk());
    • Updating other parts of the UI based on the bound bean:
      We don't add any API to Binder to simplify non-field cases that might now be handled using ReadOnlyHasValue. Instead, we assume there's already a signal containing the currently edited bean and recommend binding directly to that signal. If such a signal doesn't exist, then the first step is to add one.

      var editedPerson = new ValueSignal<>(Person.class);
      add(new Span(() -> "Editing: " + editedPerson.value().getName()));
    • Two-way field bindings:
      Binder is overkill for simple cases such as a standalone search field. Instead, the natural approach would be to bind a signal directly to the value of an input field. This kind of binding would be most natural as a two-way binding so that the developer doesn't have to do a React-style value change listener that only updates the signal value.

       var text = new ValueSignal<>("");
       add(new TextField() {{
           bindValue(text);
       }});
       add(new Span() {{
           bindText(() -> "You typed: " text.value());
       }});
       add(new Button("Clear", click -> text.value(""));
  • Iteration:

    • The general assumption for iteration is that there's a container component that should have one component child of some specific type for each child signal in a ListSignal (or a computed signal with the same value type). To help implement this pattern, HasComponents interface shall provide a void bindComponentChildren method along with a similar bindElementChildren method in Element, that takes the bound list signal and the mapping function between signal value and rendered component instance. It's under question what signal type and what generic type is better to use for the method signature, it should be decided during implementation and API validation, but the general considerations are:
      • method shall have few overrides that allow using ReferenceSignal and ValueSignal depending on the need of concurrent modification
      • method shall allow using ListSignal as well as Signal<List<S extends WritableSignal>> or Signal<List<ValueSignal<T>> because ListSignal uses JSON representation for the value and that is not necessary in non-concurrent context.
    • These methods and bindText are mutually exclusive so that each one throws a BindingActiveException if any binding is present. While bound, methods for appending or removing children also throw, as does removeFromParent for any child that is attached through a binding
    • Helper method ComponentEffect::bindChildren is for use with components that don't provide appropriate high-level methods with signal support. Like with using ComponentEffect for component properties, the use of ComponentEffect in this case does not make other methods throw BindingActiveException
  • Reactive callbacks:

    • Some components have configuration in the form of a callback that is used to provide some configuration on demand. This pattern is most common in components that implement HasItems but it can also be seen e.g. with the highlight condition in RouterLink. If any such callback reads a signal value, then it the callback should automatically run again and the outcome applied whenever the signal value changes:

      grid.setItemSelectableProvider(item -> {
          if (item.isSensitive()) {
              return sensitiveSelectionAllowedSignal.value();
          } else {
              return true;
          }
      });
    • Simple cases like the router link highlight condition can be handled with some additional API in SignalPropertySupport. While out of scope for this PRD, extra care is needed for cases with lazy loaded items to ensure listeners are cleared when an item is no longer in the active range and also to avoid inflating memory use for the most common case with callbacks that don't read any signal value.

    • This mechanism should not be used for callbacks that are used to handle events – it's not expected that an event handler runs again unless the actual event is triggered again.

    • Other examples of methods in components that follow this pattern and should support signal binding:

      • Grid::setDropFilter(SerializablePredicate<T> dropFilter)
      • GridContextMenu::setDynamicContentHandler(SerializablePredicate<T> dynamicContentHandler)
      • GridPro.EditColumn::setCellEditableProvider(SerializablePredicate<T> cellEditableProvider)
      • Select::setItemEnabledProvider(SerializablePredicate<T> itemEnabledProvider)
    • See the "Reactive Callback" column in the full list of methods to be modified.

  • Selective rendering:

    • It shall be possible to dynamically render a single component out of a components option list based on a signal's value. A value can be of arbitrary type depending to a toggle condition, e.g. window resize, user permissions, edit mode, loading state etc.

    • The binding shall be done through the new helper ComponentToggle component as shown below (window width case, lazy-created component instances):

    // widthSignal value can listen for resize event in Page::addBrowserWindowResizeListener
    ComponentToggle<Integer> toggle = new ComponentToggle<>(widthSignal);
    layout.add(toggle);
    toggle.addExclusive(BigComponent::new, width -> width > 1000);
    toggle.addExclusive(MediumComponent::new, width -> width > 500);
    toggle.addFallback(SmallComponent::new);
    • ComponentToggle picks a single component out of lazy-creating component instances (as above) or a set of existing instances:
    ComponentToggle toggle = new ComponentToggle(smallComponent);
    toggle.addExclusive(bigComponent, () -> widthSignal.value() > 1000);
    toggle.addExclusive(mediumComponent, () -> widthSignal.value() > 500);
    • ComponentToggle does not have a client part, semantically similar to Composite and thus shall be placed into the Flow core module. It has no tag in the DOM. Due to these complexities, it's better to add the base feature into framework first that then makes adding ComponentToggle easier, see Create a ConditionalComponent flow#22031.
  • Multi-User Collaboration

    • Signals API is collaborative-ready by design within same JVM, but an extra validation is needed to test various scenarios like chat/message list, concurrent state modification, collaborative form editing.

Validation, DX and use cases under consideration

Greenfield validation of the new API should be run. For this, a list of use cases has been defined and we shall review and improve the generated code then answer ourselves the following:

  • are Signals best way to implement the use case and shall we keep considering that use case,
  • is the use case doable with the API that we plan to complete within a project, discuss and create new API requests otherwise,
  • does the code look useful, simpler than the classical approach and performant, make changes to the design or create bugs otherwise.

In addition to that, we shall aim to run DX tests (internal and, if possible, external) to validate how much Signals are appropriate for the new development and converting existing app.

Risks, limitations and breaking changes

Risks

Besides the requirements listed above, the goal of the project is to validate the new API against real-world use cases. Thus, the scope and design may change as we advance with development and validation. We should start testing the API as soon as possible to have time to change/supplement API accordingly. The testing plan/validation guidelines are given as a separate section.

Limitations

We recommend to use Signals for cases where a state change finally ends up on the UI. We are not considering cases where Signals are used outside a UI context, i.e. in the pure backend tasks, though such cases may work fine and give some benefits.

Breaking changes

Breaking changes in Signal and related classes in core are possible as the API is under the feature flag. Breaking changes in existing core or components API should be avoided.

Out of scope of 25.1, but must be considered later

  • Helpers for debugging Signals
  • Binding to items in HasItems / DataProvider components
  • Router integration
  • Clustering

Materials

No response

Metrics

No response

Pre-implementation checklist

  • Estimated (estimate entered into Estimate custom field)
  • Product Manager sign-off (Artur)
  • Engineering Manager sign-off (Mikhail)

Pre-release checklist

  • Documented (link to documentation provided in sub-issue or comment)
  • UX/DX tests conducted and blockers addressed
  • Approved for release by Product Manager

Security review

None

Sub-issues

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    Status

    March 2026 (25.1)

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions