- Introduction
- Core Concepts
- Signal Types
- Signal Factory
- Usage Examples
- Best Practices
- Advanced Topics
- Signal Binding in Element
Introduction
Signals enable reactive state management solution for Vaadin Flow applications. With reactive state management, the state of a view or component is explicitly declared and components are configured to automatically update themselves when the state changes. This helps make UI state management less complicated in general and is particularly useful for sharing state between multiple users in a thread-safe manner. Signals are reactive by design, automatically updating dependent parts of the UI when their values change.
|
Note
|
Preview Feature
This is a preview version of Signals. You need to enable it with the feature flag |
Key Features
-
Reactive: Changes to signal values automatically propagate to dependent parts of the UI.
-
Immutable values: Signals work best with immutable values, e.g. String or Java Records, to ensure data consistency.
-
Hierarchical data structures: Signals can represent complex hierarchical data structures.
-
Transactions: Multiple operations can be grouped into a transaction that either succeeds or fails as a whole.
-
Thread-safe by design: Signals are designed to handle concurrent access from multiple users.
-
Atomic operations: Signals support atomic updates to ensure data consistency.
Core Concepts
Signals
A signal is a holder for a value. When the value of a signal value is changed, all dependent parts are automatically updated without the need to manually add and remove change listeners.
Effects
Effects are callbacks that automatically re-run when any signal value they depend on changes. Dependencies are automatically managed based on the signals that were used the last time the callback was run.
Effects are used to update the UI in response to signal changes. The effect is defined in the context of a UI component. The effect is inactive while the component is detached and active while the component is attached.
Source code
Java
ComponentEffect.effect(span, () -> {
// This code will run whenever any signal value used inside changes
span.setText(firstNameSignal.value() + " " + lastNameSignal.value());
});Computed Signals
Computed signals derive their values from other signals. They are automatically updated when any of the signals they depend on change.
Source code
Java
Signal<String> fullName = Signal.computed(() -> {
return firstNameSignal.value() + " " + lastNameSignal.value();
});Transactions
Transactions allow grouping multiple signal operations into a single atomic unit. All operations in a transaction either succeed or fail together.
Source code
Java
Signal.runInTransaction(() -> {
// All operations here will be committed atomically
firstNameSignal.value("John");
lastNameSignal.value("Doe");
});Signal Types
Several signal types are available for different use cases:
ValueSignal
A signal containing a single value. The value is updated as a single atomic change.
Source code
Java
ValueSignal<String> name = new ValueSignal<>(String.class);
name.value("John Doe"); // Set the value
String currentName = name.value(); // Get the valueNumberSignal
A specialized signal for numeric values with support for atomic increments and decrements. The signal value is represented as a double and there are methods to access the value as an int.
Source code
Java
NumberSignal counter = new NumberSignal();
counter.value(5); // Set the value
counter.incrementBy(1); // Increment by 1
counter.incrementBy(-2); // Decrement by 2
int count = counter.valueAsInt(); // Get the value as intListSignal
A signal containing a list of values. Each value in the list is accessed as a separate ValueSignal.
Source code
Java
ListSignal<Person> persons = new ListSignal<>(Person.class);
persons.insertFirst(new Person("Jane", 25)); // Add to the beginning
persons.insertLast(new Person("John", 30)); // Add to the end
List<ValueSignal<Person>> personList = persons.value(); // Get all persons
personList.get(0).value(new Person("Bob", 20)); // Update the value of a child signalMapSignal
A signal containing a map of values with string keys. Each value in the map is accessed as a separate ValueSignal.
Source code
Java
MapSignal<String> properties = new MapSignal<>(String.class);
properties.put("name", "John"); // Add or update a property
properties.putIfAbsent("age", "30"); // Add only if not present
Map<String, ValueSignal<String>> propertyMap = properties.value(); // Get all propertiesNodeSignal
A signal representing a node in a tree structure. A node can have its own value and child signals accessed by order or by key. A child node is always either a list child or a map child, but it cannot have both roles at the same time.
Source code
Java
NodeSignal user = new NodeSignal();
user.putChildWithValue("name", "John Doe"); // Add a map child
user.putChildWithValue("age", 30); // Add another map child
user.insertChildWithValue("Reading", ListPosition.last()); // Add a hobby as a list child
user.value().mapChildren().get("name").asValue(String.class).value(); // Access child value 'John Doe'
user.value().mapChildren().get("age").asValue(Integer.class).value(); // Access child value 30
user.value().listChildren().getLast().asValue(String.class).value(); // Access last list child value 'Reading'
MapSignal<String> mapChildren = user.asMap(String.class); // Access all map children
mapChildren.value().get("name"); // Alternative way of accessing child value 'John Doe'Signal Factory
The SignalFactory interface provides methods for creating signal instances based on a string key, value type and initial value. It supports different strategies for creating instances:
IN_MEMORY_SHARED
Returns the same signal instance for the same name within the same JVM. This is similar to running the respective constructor to initialize a static final field.
Source code
Java
NodeSignal shared = SignalFactory.IN_MEMORY_SHARED.node("myNode");IN_MEMORY_EXCLUSIVE
Always creates a new instance. Directly running the respective constructor typically leads to clearer code but this factory can be used in cases where the same method supports multiple strategies.
Source code
Java
NodeSignal exclusive = SignalFactory.IN_MEMORY_EXCLUSIVE.node("myNode");The SignalFactory interface is the extension point for creating custom signal factories. Additional factory implementations are planned for creating signal instances that are shared across multiple JVMs in a cluster.
Usage Examples
Simple Counter Example
This example demonstrates how to bind a counter signal (state) to a button (UI) — the button’s text is updated reactively based on the counter value.
The binding between state and UI is done using a ComponentEffect.effect helper. In this case, it creates an effect that uses the value from counter signal to update button text whenever the signal changes.
Source code
Java
public class SimpleCounter extends VerticalLayout {
// gets a signal instance that is shared across the application
private final NumberSignal counter =
SignalFactory.IN_MEMORY_SHARED.number("counter");
public SimpleCounter() {
Button button = new Button();
button.addClickListener(
// updates the signal value on each button click
click -> counter.incrementBy(1));
add(button);
// Effect that updates the button's text whenever the counter changes
ComponentEffect.effect(button,
() -> button.setText(String.format("Clicked %.0f times", counter.value())));
}
}Text Field Example
Source code
Java
public class SharedText extends FormLayout {
private final ValueSignal<String> value =
SignalFactory.IN_MEMORY_SHARED.value("value", "");
public SharedText() {
TextField field = new TextField("Value");
ComponentEffect.bind(field, value, TextField::setValue);
field.addValueChangeListener(event -> {
// Only update signal if value has changed to avoid triggering infinite loop detection
if (!event.getValue().equals(value.peek())) {
value.value(event.getValue());
}
});
add(field);
}
}ComponentEffect.bind is a helper function that does the same as this explicitly defined effect:
Source code
Java
ComponentEffect.effect(field,
() -> field.setValue(value.value()));Note that you need to enable push for your application to ensure changes are pushed out for all users immediately when one user makes a change.
List Example
Source code
Java
public class PersonList extends VerticalLayout {
private final ListSignal<String> persons =
SignalFactory.IN_MEMORY_SHARED.list("persons", String.class);
public PersonList() {
Button addButton = new Button("Add Person", click -> {
persons.insertFirst("New person");
});
Button updateButton = new Button("Update first Person", click -> {
ValueSignal<String> first = persons.value().get(0);
first.update(text -> text + " updated");
});
UnorderedList list = new UnorderedList();
ComponentEffect.effect(list, () -> {
list.removeAll();
persons.value().forEach(personSignal -> {
ListItem li = new ListItem();
ComponentEffect.bind(li, personSignal, ListItem::setText);
list.add(li);
});
});
add(addButton, updateButton, list);
}
}Removing all list items and creating them again is not the most efficient solution. A helper method will be added later to bind child components in a more efficient way.
The effect that creates new list item components will be run only when a new item is added to the list but not when the value of an existing item is updated.
Best Practices
Use Immutable Values
Signals work best with immutable values. This ensures that changes to signal values are always made through the signal API, which maintains consistency and reactivity.
Source code
Java
ValueSignal<User> user = new ValueSignal<>(User.class);
// Good: Creating a new immutable object
user.update(u -> new User(u.getName(), u.getAge() + 1));
// Bad: Modifying the object directly
User u = user.value();
u.setAge(u.getAge() + 1); // This won't trigger reactivity!Use Component Effects for UI Updates
Various helper methods simplify binding of signals to components:
Source code
Java
// Bind an effect function to a component:
ComponentEffect.effect(myComponent, () -> {
Notification.show("Component is attached and signal value is " + someSignal.value());
});
// Bind an effect function to a component using a value from a give signal:
ComponentEffect.bind(label, user.map(u -> u.getName()), Span::setText);
ComponentEffect.bind(label, stringSignal, Span::setText);
ComponentEffect.bind(label, stringSignal.map(value -> !value.isEmpty()), Span::setVisible);Use Transactions for Atomic Updates
Use transactions when you need to update multiple signals atomically. All changes from the transaction are applied atomically so that no observer can see a partial update. If any change fails, then none of the changes are applied.
Source code
Java
Signal.runInTransaction(() -> {
firstName.value("John");
lastName.value("Doe");
age.value(30);
});Use update() for Atomic Updates Based on Current Value
Use the update() method when you need to update a signal’s value based on its current value.
Source code
Java
counter.update(current -> current + 1);Avoid changing signal values from inside effect or computed signal callbacks
Updating the value of a signal as a direct reaction to some other signal value change might cause an infinite loop. To help protect against this, effect and computed signal callbacks are run inside a read-only transaction to prevent any accidental changes.
Whenever possible, you should create a computed signal for any case where the value of some signal affects the value of another signal.
Source code
Java
Signal<String> otherSignal = Signal.computed(() -> {
return oneSignal.value();
});If that is not possible and you are certain there’s no risk for infinite loops, you can bypass the check by using the runWithoutTransaction method.
Source code
Java
ComponentEffect.effect(() -> {
String value = oneSignal.value();
// This might lead to infinite loops.
// Do this only if absolutely necessary.
Signal.runWithoutTransaction(() -> {
otherSignal.value(value);
});
});Advanced Topics
Standalone effects
A standalone signal effect can be used for effects that aren’t related to any UI component.
The effect remains active until explicitly cleaned up. This might lead to memory leaks through any instances referenced by the closure of the effect callback.
Source code
Java
Runnable cleanup = Signal.effect(() -> {
System.out.println("Counter updated to " + counter.value());
});
// Later, when the effect is no longer needed
cleanup.run();Signal Mapping
You can transform a signal’s value using the map() method. This is a shorthand for creating a computed signal that depends on exactly one other signal.
Source code
Java
ValueSignal<Integer> age = SignalFactory.IN_MEMORY_SHARED.value("age", Integer.class);
Signal<String> ageCategory = age.map(a ->
a < 18 ? "Child" : (a < 65 ? "Adult" : "Senior"));Read-Only Signals
You can create read-only versions of signals that don’t allow modifications. The original signal remains writeable and the read-only instance is also updated for any changes made to the original instance.
Source code
Java
ValueSignal<String> name = SignalFactory.IN_MEMORY_SHARED.value("name", String.class);
ValueSignal<String> readOnlyName = name.asReadonly();Signal Binding in Element
Various com.vaadin.flow.dom.Element features support signal binding, such as text, attributes, properties, ClassList, and Style. See Element API for more details of those features.
Each feature works with same rules. When feature is bound to a signal, feature’s value, like text content, is kept synchronized with the signal value while the element is in the attached state. When the element is in detached state, signal value changes have no effect. null signal unbinds the existing binding. While a signal is bound, any attempt to set the value manually in other way than through the signal, throws BindingActiveException. The same happens when trying to bind a new signal while one is already bound.
The following code serves as the base for the examples in the subsequent sections. It adds a button that increments a number signal by one and an empty span element:
Source code
Java
public class SimpleCounter extends VerticalLayout {
private final NumberSignal counter =
SignalFactory.IN_MEMORY_SHARED.number("counter");
public SimpleCounter() {
Button button = new Button("Increment by one");
button.addClickListener(
click -> counter.incrementBy(1));
add(button);
Span span = new Span("");
add(span);
}
}Text Binding
Source code
Element#bindText(Signal<String> signal)
Element#bindText(Signal<String> signal)// NumberSignal's Double type has to be mapped to String.
Signal<String> signal = counter.map(value -> String.format("Clicked %.0f times", value));
span.getElement().bindText(signal);
// span's text content is now "Clicked 0 times"Source code
Basic functionality (same rules with all Element bindings)
// The following code demonstrates behavior step by step:
span.getElement().getText(); // returns "Clicked 0 times"
span.getElement().setText(""); // throws BindingActiveException
span.getElement().removeFromParent(); // detaching from the UI
span.getElement().getText(); // returns "Clicked 0 times"
span.getElement().setText(""); // throws BindingActiveException
counter.value(5); // updating the signal value
span.getElement().getText(); // returns "Clicked 0 times"
add(span); // re-attaching the element to the UI
span.getElement().getText(); // returns "Clicked 5 times"
span.getElement().bindText(null); // unbinds the existing binding
span.getElement().getText(); // returns "Clicked 5 times"
span.getElement().setText("");
span.getElement().getText(); // returns ""
counter.value(0);
span.getElement().getText(); // returns ""Attribute Binding
Source code
Element#bindAttribute(String attribute, Signal<String> signal)
Element#bindAttribute(String attribute, Signal<String> signal)ValueSignal<Boolean> hidden = new ValueSignal<>(false);
span.getElement().bindAttribute("hidden", hidden.map(value -> value ? "" : null));
// DOM has "<span hidden>".
hidden.value(!hidden.peek());
// DOM has "<span>" now.
// Boolean is mapped to "" when true and null when false.
// Some other value like 'foo' would be "<span hidden='foo'>".Property Binding
Supports various value types: String, Boolean, Double, BaseJsonNode, Object (bean), List and Map.
Typed Lists and Maps are not supported, i.e. the signal must be of type Signal<List<?>> or Signal<Map<?,?>.
Source code
Element#bindProperty(String name, Signal<?> signal)
Element#bindProperty(String name, Signal<?> signal)ValueSignal<Boolean> hidden = new ValueSignal<>(false);
span.getElement().bindProperty("hidden", hidden);
hidden.value(!hidden.peek()); // toggles 'hidden' propertySource code
String type
ValueSignal<String> title = new ValueSignal<>("Hello");
span.getElement().bindProperty("title", title);
title.value("World"); // updates 'title' propertySource code
Double type
NumberSignal width = new NumberSignal();
width.value(100.5);
span.getElement().bindProperty("width", width);
width.incrementBy(50); // updates 'width' property to 150.5Source code
Object (bean) type
record Person(String name, int age) {
}
ValueSignal<Person> person = new ValueSignal<>(new Person("John", 30));
span.getElement().bindProperty("person", person);
person.value(new Person("Jane", 25));
// element.person is now {name: 'Jane', age: 25}Source code
List type
ValueSignal<List<String>> items = new ValueSignal<>(List.of("Item 1", "Item 2"));
span.getElement().bindProperty("items", items);
items.value(List.of("Item A", "Item B", "Item C"));
// element.items is now ['Item A', 'Item B', 'Item C']Source code
Map type
ValueSignal<Map<String, String>> config = new ValueSignal<>(Map.of("key1", "value1"));
span.getElement().bindProperty("config", config);
config.value(Map.of("key1", "value1", "key2", "value2"));
// element.config is now {key1: 'value1', key2: 'value2'}Source code
BaseJsonNode type
ObjectMapper mapper = new ObjectMapper();
ObjectNode jsonNode = mapper.createObjectNode();
jsonNode.put("key", "value");
ValueSignal<ObjectNode> jsonSignal = new ValueSignal<>(jsonNode);
span.getElement().bindProperty("jsonData", jsonSignal);
ObjectNode updatedJson = mapper.createObjectNode();
updatedJson.put("key", "updatedValue");
updatedJson.put("newKey", "newValue");
jsonSignal.value(updatedJson);
// element.jsonData is now {key: 'updatedValue', newKey: 'newValue'}Source code
Property change listener for 'change' DOM event
// Adds a property change listener and configures 'hidden' property
// to be synchronized to the server when 'change' DOM event is fired.
span.getElement().addPropertyChangeListener("hidden", "change", event -> {
Notification.show("'hidden' property changed to: " + event.getValue());
});
// property change event from the client will update the signal value
// Example javascript that dispatches change event from the browser where
// element is <span>:
// element.hidden = !element.hidden;
// element.dispatchEvent(new Event('change'));ClassList Binding
Source code
ClassList#bind(String name, Signal<Boolean> signal)
ClassList#bind(String name, Signal<Boolean> signal)ValueSignal<Boolean> foo = new ValueSignal<>(false);
ValueSignal<Boolean> bar = new ValueSignal<>(true);
span.getElement().getClassList().bind("foo", foo);
span.getElement().getClassList().bind("bar", bar);
// DOM has "<span class='bar'>"
foo.value(true);
// DOM has "<span class='bar foo'>"
span.getElement().getClassList().clear();
// DOM has "<span class>". Binding is also removed.Style Binding
Source code
Style#bind(String name, Signal<Boolean> signal)
Style#bind(String name, Signal<Boolean> signal)ValueSignal<String> color = new ValueSignal<>("black");
ValueSignal<String> background = new ValueSignal<>("white");
span.getElement().getStyle().bind("color", color);
span.getElement().getStyle().bind("background", background);
// DOM has "<span style='color: black; background: white'>"
color.value("red");
background.value("gray");
// DOM has "<span style='color: red; background: gray'>"
background.value(""); // same with null
// DOM has "<span style='color: red;'>"
span.getElement().getStyle().clear();
// DOM has "<span style>". Binding is also removed.SignalPropertySupport helper
Not all component features delegate directly to the state in com.vaadin.flow.dom.Element. For those features, the SignalPropertySupport helper class ensures that state management behaves consistently with other Element bindings.
For example, a component can have a Java API to bind Signal<String> to modify the textContent property of the element in the browser. SignalPropertySupport#create(Component, SerializableConsumer<T>) creates a new instance of SignalPropertySupport with bind(Signal<T>), get(), and set(T) methods. The given consumer sets the textContent based on the value, as shown in the following example.
Source code
MyComponent creation
// The following code demonstrates behavior step by step:
MyComponent component = new MyComponent();
component.bindTextContent(counter.map(v -> "Signal value: " + v));
add(component);
// textContent in browser is "Content: Signal value: 0.0"
component.getTextContent(); // returns "Signal value: 0.0"
component.setTextContent(""); // throws BindingActiveException
component.bindTextContent(null); // unbinds the existing binding
component.setTextContent("");
component.getTextContent(); // returns ""
// textContent in browser is "Content: "Source code
SignalPropertySupport usage
class MyComponent extends Div {
private final SignalPropertySupport<String> textProperty =
SignalPropertySupport.create(this, value -> {
getElement().executeJs("this.textContent = 'Content: ' + $0", value);
});
public String getTextContent() {
return textProperty.get();
}
public void setTextContent(String text) {
textProperty.set(text);
}
public void bindTextContent(Signal<String> textSignal) {
textProperty.bind(textSignal);
}
}