One very exciting new feature in Vaadin 24.8 is the preview of signals for reactive UI state management. What makes this feature so revolutionary is that it introduces full reactive UI state management to Flow. You can set up any component instance to configure itself based on some signal values and then the component automatically stays in sync with the signals.
Right now is an excellent opportunity for the eager ones in the crowd to get some first impressions and help us move this initiative in the right direction. We really want to make sure we got this one right before we remove the feature flag and declare it ready for general use. Just keep in mind that there are some part that we know are still lacking (more about those below) and that will need to be covered before signals can be the default solution for all your UI state management needs.
Hello signal world
You can try signals with Flow in any application that uses Vaadin 24.8, e.g. from https://start.vaadin.com/. You need to enable the “Flow Full-stack Signals” feature flag either through the Features section in the right side panel that is enabled from the }>
development menu when running the application, or by manually adding com.vaadin.experimental.flowFullstackSignals=true
to src/main/resources/vaadin-featureflags.properties
.
You are now ready to create a simple view with a single piece of state: a counter tracking how many times a button has been clicked:
@Route @Menu @PermitAll
public class Signals extends VerticalLayout {
private final NumberSignal count = new NumberSignal();
public Signals() {
Button button = new Button();
button.addClickListener(event -> count.incrementBy(1));
ComponentEffect.format(button, Button::setText, "Click count: %.0f", count);
add(button);
}
}
All of the magic is in the ComponentEffect
helper method that sets things up to do something equivalent to button.setText("Click count: %.0f".formatted(count.value()))
whenever the value of the count
signal changes.
The real power comes from the fact that you can combine signals and component effects in arbitrary ways. As another simple example, you can create a computed signal that checks whether the click count is even or odd and then shows and hides a Span
based on this. The magic is in the fact that you just use the same state in multiple places without having to make any changes to the click listener that updates that state.
Signal<Boolean> isEven = Signal.computed(() -> count.valueAsInt() % 2 == 0);
Span evenSpan = new Span("Count is even");
ComponentEffect.bind(evenSpan, isEven, Span::setVisible);
add(evenSpan);
Two-way bindings
Signals also shine in cases where data is bound both ways, i.e. anything related to forms. This is one area where we will add some helper functionality to the framework but you can also do things relatively easily with just the low-level building blocks. The key trick for now is to avoid triggering the infinite loop detection by trying to change the signal value only if the value has actually changed.
ValueSignal<String> text = new ValueSignal<>("Initial value");
TextField field = new TextField("Enter some text");
field.addValueChangeListener(event -> {
if (!Objects.equals(event.getValue(), text.peek())) {
text.value(event.getValue());
}
});
ComponentEffect.bind(field, text, TextField::setValue);
Span valueSpan = new Span();
ComponentEffect.format(valueSpan, Span::setText, "Value is: %s", text);
Button setValue = new Button("Set value", click -> text.value("Set from button"));
add(field, valueSpan, setValue);
One very powerful example of what we can do with this is automatic cross-field validation. As a completely made-up use case, let’s say that if the click count is even, then the number of characters in the text field also have to be even. There’s a little bit of boilerplate code due to directly manipulating the field status rather than using Binder
. The key thing to notice is the way the validator logic will automatically run again whenever either of the two dependencies change.
field.setManualValidation(true);
ComponentEffect.effect(field, () -> {
boolean evenClick = isEven.value();
boolean evenText = text.value().length() % 2 == 0;
field.setInvalid(evenClick == evenText);
field.setErrorMessage("Length must be " + (evenClick ? "even" : "odd"));
});
Collaboration
Last year when we introduced “full-stack signals” for Hilla (still in preview) there was lots of focus on collaboration and live updates from the server. You can certainly also use signals in Flow for the same purpose even though that’s just cherry on the top rather than the main thing in this case.
Component updates based on signal changes are automatically synchronized using UI.access
behind the scenes whenever necessary. This means that you can share signal instances for any case where you want to share UI state between users and everything will just automatically work.
The simplest way of testing this out is to change the count
instance field into a static
field so that all instances of the view use the same signal instance. We also need to enable server push by adding the @Push
annotation to the Application
class.
private static final NumberSignal count = new NumberSignal();
FAQ
ComponentEffect
use looks verbose. Couldn’t that functionality be included in component classes?
We should add short hand APIs to enable things like e.g. span.setVisible(someSignal)
or span.setText("Value is: %s", text)
. We expect to do that once we have gained some confidence in the core idea.
How can I populate the UI based on a list?
There’s a ListSignal
class of managing list data in an efficient way. We plan to introduce a helper along the lines of ComponentEffect.bindChildren(parent, listSignal, childSignal -> createComponent(childSignal))
that would take care of attaching, detaching and moving child components efficiently.
Until that, you can use this quite inefficient solution that re-creates all components for any structural change:
ComponentEffect.effect(parent, () -> {
parent.removeAll();
listSignal.value().forEach(childSignal -> parent.add(createComponent(childSignal)));
});
Why do I get an error about read-only transactions?
To help protect against infinite update loops, you’re not allowed to update any signal from inside an effect
or computed
callback since they are run in reaction to some other signal changing. This is implemented as a “read-only transaction” which leads to that somewhat confusing error message.
This typically happens when you create a two-way binding since changing the signal value will trigger the effect which in turn triggers the value change listener that tries to update the signal value. We plan to provide a better way of creating two-way bindings but as a workaround for now, you can use the same Objects.equals
condition in the value change listener that I showed in one of the examples.
Fixing the problem in the general case usually requires you to re-think what makes up the “primary” or “authoritative” state from which everything else can be derived and then introducing computed signals for anything that can be derived.
How does this relate to Hilla?
The full-stack signal implementation in Hilla uses Java types in the com.vaadin.hilla.signals
package. These types are not compatible with the com.vaadin.signals
types that are used with Flow. We will update Hilla to use com.vaadin.signals
in the future.
This looks cool. Where can I find out more?
Just ask here.
You can also have a look at the reference documentation for a more comprehensive overview of all the key features.
(This one probably goes in the category of “frequently wished questions”, but I just needed some place to put that link.)