I’ve written this text as a general description of the benefits that you get from the reactive UI state management approach that we enable with signals. This text might eventually be used as the basis for a blog post or a documentation article but I’m posting it right away here in the forum to encourage the community to try out signals with the new Vaadin 25.1.0-beta1 release.
From component updates to state updates
For its first 25 years Vaadin has been based on updating components from listeners. This is a very powerful mechanism, but it can also become tedious especially in cases when the same component is updated from many listeners or when one listener updates many components. We’re now introducing a new model where state is at the center of driving the UI. The main task of listeners is to update the state which in turn automatically triggers component updates.
Let’s look at a couple of examples to understand what this means in practice: shopping carts, cross-field validation, and background updates.
Listeners make you build your own framework
The most common challenge with a directly updating components can be seen in a typical shopping cart example. An event listener is triggered when the user clicks a button to add a product to the cart. This listener needs to create and attach a new UI component to show the product in the cart and update the total order value along with subtotals, shipping costs and taxes. Then there’s another listener when the user clicks the button to remove a product from their cart and this listener needs to do all the same things but in reverse.
Add to this quantity selectors for each line in the cart, discount codes, different shipping methods, and all the other things essential to a good shopping experience. The end result is that there might be ten different listeners that all affect the total cart price in different ways.
A seasoned Vaadin developer has collected a toolbox of patterns that help tame this complexity. For the shopping cart example, they might implement a single updateTotals() method that they call whenever anything has changed instead of directly updating the same text label separately from each listener. They are basically building their own implicit framework to supplement what’s been missing from Vaadin’s framework. A new user might instead spend lots of time trying to come up with a strategy that is appropriate for each case.
Automatic state management lets listeners focus on semantics
This is an opportunity to raise the level of abstraction for all Vaadin users. Updating UI components based on events from the user is a cross-cutting concern with very similar requirements in all applications. Building this into the core framework enables a more efficient implementation while also making it easier to use with helpers in all component APIs. New users don’t need to assemble their own toolbox and existing users can focus their efforts on what’s unique to their application.
When listeners update state, the shopping cart example revolves around the state that describes the cart content. Event listeners update the primary state of the cart while derived state and component configurations are updated automatically. The listener for adding a new item to the cart can be just the Java version of the English description: cart.insertLast(new CartItem(product, quantity));. The listener doesn’t need to be concerned about adding new UI components or updating existing ones. This is similar to the Stream API in Java where application code defines the desired result but not the exact mechanism for achieving that.
The application still needs to define how that state is represented in the UI. There should be one visual row in the UI for each item in the cart list. The total value label is computed based on the sum of each individual item and adjusted with taxes, shipping fees, and so on. These bindings from state to component configuration can and should be defined in the same part of the code that configures any static configuration for the same component. In that way, everything related to one component is in one place rather than spread out across listeners or helper methods like updateTotals().
Another interesting example is cross-field validation where changes to one input field in a form might affect the validation status for another field. When updating components from listeners, the field has a listener that tells the form binding library that the validation status of the other field needs to be evaluated again. In a state-centric approach, the library keeps track of which parts of the state are used by each validator and can then automatically run the right validation again when appropriate.
Cherry on the top: state can be updated from wherever you want
One side effect of updating components from listeners is that updates from sources other than direct UI events become second-class citizens. This happens because any listener might update any part of the UI which in turn means that locking is needed to avoid race conditions. The framework can handle this locking transparently for events triggered directly by the user but separate locking is needed to update the UI from a background thread or from an event triggered by a different user.
There’s more flexibility for dealing with changes from different sources when listeners update state instead. The application only needs to be concerned about potential race conditions affecting the application’s own state. That can be handled on a case-by-case basis and often with simpler mechanisms such as atomically replacing immutable value references. In comparison, a component tree with listeners requires coarse-grained locking for things like ensuring that a component actually remains attached while an attach handler runs.
When the component tree is updated through state bindings instead of directly from listeners, the framework can handle all the locking necessary to protect the component tree. The application itself can then update the state in the same simple way regardless of the source of the change.
This is yet another example where new Vaadin users have typically struggled while seasoned users have gradually built their own ad-hoc framework based on their past mistakes. Background updates weren’t the main reason for shifting to a state-centric model but it was a very happy day when we realized that we get this benefit as an added bonus.
Abstractions build on top of each other
As we have seen, putting state in the center gives several benefits compared to directly updating components. At the same time, it’s important to empathize that the state updates build on top of component updates. There are still cases where the right solution is to directly update components. All existing component-updating application code keeps working in the same way as before, while new code can get benefits from the higher abstraction level.