Skip to content

feat: Add Flow Signal documentation #4353

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jun 19, 2025
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
393 changes: 393 additions & 0 deletions articles/flow/advanced/signals.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,393 @@
---
title: Reactive UI with Signals
page-title: How to create reactive UIs with the Flow Signals library

Check failure on line 3 in articles/flow/advanced/signals.adoc

View workflow job for this annotation

GitHub Actions / lint

[vale] reported by reviewdog 🐶 [Vale.Spelling] Did you really mean 'UIs'? Raw Output: {"message": "[Vale.Spelling] Did you really mean 'UIs'?", "location": {"path": "articles/flow/advanced/signals.adoc", "range": {"start": {"line": 3, "column": 36}}}, "severity": "ERROR"}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume most readers don't know what a "reactive UI" is. The page title could instead be about "How to manage UI state".

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

description: Using the Flow Signals library to manage UI state reactively in Vaadin applications.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's an inconsistency between "the Flow Signals library" and "the Flow Signal library".

At the same time, I don't think it's relevant to give the name or emphasize that it's a library. From the user's point of view, it's just part of Flow just like the view router or the form binder (which are capitalized mainly when referring to the specific class but not when referring to the concept as a whole). I would thus prefer to use "signals in Flow" or just "signal".

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. Always be conservative when thinking about introducing new named concepts to the products. First try to describe/document them with common language, and if that becomes unwieldy or hard to comprehend, then you can try introducing a new named concept, that you very clearly define somewhere in the documentation.

meta-description: Manage UI state reactively and share it within an application or cluster with Vaadin Flow Signals library.
version: since:com.vaadin:vaadin@V24.8

Check failure on line 6 in articles/flow/advanced/signals.adoc

View workflow job for this annotation

GitHub Actions / lint

[vale] reported by reviewdog 🐶 [Vale.Terms] Use '(?-i)Vaadin' instead of 'vaadin'. Raw Output: {"message": "[Vale.Terms] Use '(?-i)Vaadin' instead of 'vaadin'.", "location": {"path": "articles/flow/advanced/signals.adoc", "range": {"start": {"line": 6, "column": 27}}}, "severity": "ERROR"}

Check failure on line 6 in articles/flow/advanced/signals.adoc

View workflow job for this annotation

GitHub Actions / lint

[vale] reported by reviewdog 🐶 [Vale.Terms] Use '(?-i)Vaadin' instead of 'vaadin'. Raw Output: {"message": "[Vale.Terms] Use '(?-i)Vaadin' instead of 'vaadin'.", "location": {"path": "articles/flow/advanced/signals.adoc", "range": {"start": {"line": 6, "column": 20}}}, "severity": "ERROR"}
order: 125
---

== Introduction

The Flow Signal library provides a reactive state management solution for Vaadin Flow applications.
Its ultimate goal is to enable sharing state between multiple clients and the server in a thread-safe manner.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ultimate goal in the context of Flow is to make it easier to handle UI state regardless of whether it's shared. Thread-safe sharing and collaboration are just cherries on the top.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Signals are designed to make UI state management less complicated, especially when dealing with concurrent access from multiple users.

The library offers a set of signal types that support atomic operations, conditional operations and transactions.
Signals are reactive by design, automatically updating dependent parts of the UI when their values change.

[NOTE]
Vaadin Flow Signals library is a preview feature in Vaadin 24.8 and may change in future releases.

Check warning on line 20 in articles/flow/advanced/signals.adoc

View workflow job for this annotation

GitHub Actions / lint

[vale] reported by reviewdog 🐶 [Vaadin.Versions] Don't refer to a specific Vaadin version. Raw Output: {"message": "[Vaadin.Versions] Don't refer to a specific Vaadin version.", "location": {"path": "articles/flow/advanced/signals.adoc", "range": {"start": {"line": 20, "column": 53}}}, "severity": "WARNING"}
Signals integration into components API is planned in future releases but not yet included, thus you need to add listeners to components and effect functions manually.

=== Key Features
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as in some comments above, I think the emphasis here could be shifted more towards "regular" state management.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


* *Thread-safe by design*: Signals are designed to handle concurrent access from multiple users.
* *Reactive*: Changes to signal values automatically propagate to dependent parts of the UI.
* *Atomic operations*: Signals support atomic updates to ensure data consistency.
* *Transactions*: Multiple operations can be grouped into a transaction that either succeeds or fails as a whole.
* *Hierarchical data structures*: Signals can represent complex hierarchical data structures.
* *Immutable values*: Signals work best with immutable values, e.g. String or Java Records, to ensure data consistency.

== Core Concepts

=== Signals

A signal is a reactive value holder with automatic subscription and unsubscription of listeners. When a signal's value changes, all dependent parts of the UI are automatically updated.

Check failure on line 36 in articles/flow/advanced/signals.adoc

View workflow job for this annotation

GitHub Actions / lint

[vale] reported by reviewdog 🐶 [Vale.Spelling] Did you really mean 'unsubscription'? Raw Output: {"message": "[Vale.Spelling] Did you really mean 'unsubscription'?", "location": {"path": "articles/flow/advanced/signals.adoc", "range": {"start": {"line": 36, "column": 69}}}, "severity": "ERROR"}

=== Effects

Effects are callbacks that automatically re-run when any signal value they depend on changes. They are used to update the UI in response to signal changes.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could emphasize that dependency tracking is automatic.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


[source,java]
----
Signal.effect(() -> {
// This code will run whenever any signal value used inside changes
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"any" gives the impression that multiple signals are used. Could either rephrase or change the example to use multiple signals. Could maybe use the same firstName.value() + " " + lastName.value() example here as well.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

button.setText("Counter: " + counter.value());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should really use ComponentEffect here since a component instance is involved. But we don't want to introduce that yet which means that we could do something simpler like System.out.println instead.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

});
----

=== Computed Signals

Computed signals derive their values from other signals. They are automatically updated when any of the signals they depend on change.

[source,java]
----
Signal<String> fullName = Signal.computed(() ->
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The line break makes this difficult to read. Could split up into a {} block with a return for clarity.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

firstName.value() + " " + lastName.value());
----

=== Transactions

Transactions allow grouping multiple signal operations into a single atomic unit. All operations in a transaction either succeed or fail together.

[source,java]
----
Signal.runInTransaction(() -> {
// All operations here will be committed atomically
firstName.value("John");
lastName.value("Doe");
});
----

== Signal Types

The Flow Signal library provides several signal types for different use cases:

=== ValueSignal

A signal containing a single value. The value is updated as a single atomic change.

[source,java]
----
ValueSignal<String> name = SignalFactory.IN_MEMORY_SHARED.value("name", String.class);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could make sense to show the constructor here since SignalFactory is introduced later

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

name.value("John Doe"); // Set the value
String currentName = name.value(); // Get the value
----

=== NumberSignal

A specialized signal for numeric values with support for atomic increments.

[source,java]
----
NumberSignal counter = SignalFactory.IN_MEMORY_SHARED.number("counter");
counter.value(0); // Set initial value
counter.incrementBy(1.0); // Increment by 1
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A little confusing when using an integer in one case and a floating point value in the other.
Could maybe even explicitly mention that NumberSignal is based on double but has explicit API to use only the int part of the value.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

int count = counter.valueAsInt(); // Get value as int
----

=== ListSignal

A signal containing a list of values. Each value in the list is accessed as a separate ValueSignal.

[source,java]
----
ListSignal<Person> persons = SignalFactory.IN_MEMORY_SHARED.list("persons", Person.class);
persons.insertLast(new Person("John", 30)); // Add to the end
persons.insertFirst(new Person("Jane", 25)); // Add to the beginning
List<ValueSignal<Person>> personList = persons.value(); // Get all persons
----
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could also show how to modify the value, i.e. personList.get(0).value(new Person("Bob", 20)); // Update a value

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


=== MapSignal

A signal containing a map of values with string keys. Each value in the map is accessed as a separate ValueSignal.

[source,java]
----
MapSignal<String> properties = SignalFactory.IN_MEMORY_SHARED.map("properties", 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 properties
----

=== NodeSignal

A signal representing a node in a tree structure. A node can have its own value, parent node, children signals accessed by order or by a 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,java]
----
NodeSignal user = SignalFactory.IN_MEMORY_SHARED.node("user");
user.putChildWithValue("name", "John Doe"); // Add a map child
user.putChildWithValue("age", 30); // Add another map child
user.insertChildWithValue("hobby", "Reading"); // Add a list child
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't compile. The second parameter should be a ListPosition

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


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'
----
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could also show the alternative way of accessing data using an asSomething method, e.g. user.asMap(String.class).value().get("name")

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


== Signal Factory

The [classname]`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_EXCLUSIVE

Check failure on line 144 in articles/flow/advanced/signals.adoc

View workflow job for this annotation

GitHub Actions / lint

[vale] reported by reviewdog 🐶 [Vaadin.HeadingFormatting] Don't use rich formatting in headings (bold or italic). Raw Output: {"message": "[Vaadin.HeadingFormatting] Don't use rich formatting in headings (bold or italic).", "location": {"path": "articles/flow/advanced/signals.adoc", "range": {"start": {"line": 144, "column": 1}}}, "severity": "ERROR"}

Always returns a new instance that is not shared within JVM nor cluster.

[source,java]
----
NodeSignal exclusive = SignalFactory.IN_MEMORY_EXCLUSIVE.node("myNode");
----

=== IN_MEMORY_SHARED

Check failure on line 153 in articles/flow/advanced/signals.adoc

View workflow job for this annotation

GitHub Actions / lint

[vale] reported by reviewdog 🐶 [Vaadin.HeadingFormatting] Don't use rich formatting in headings (bold or italic). Raw Output: {"message": "[Vaadin.HeadingFormatting] Don't use rich formatting in headings (bold or italic).", "location": {"path": "articles/flow/advanced/signals.adoc", "range": {"start": {"line": 153, "column": 1}}}, "severity": "ERROR"}

Returns the same signal for the same name within the same JVM.

[source,java]
----
NodeSignal shared = SignalFactory.IN_MEMORY_SHARED.node("myNode");
----

The [classname]`SignalFactory` is the extension point for creating custom signal factories. You can implement your own factory to create signals that are shared across multiple JVMs in clusters.

Check failure on line 162 in articles/flow/advanced/signals.adoc

View workflow job for this annotation

GitHub Actions / lint

[vale] reported by reviewdog 🐶 [Vale.Spelling] Did you really mean 'JVMs'? Raw Output: {"message": "[Vale.Spelling] Did you really mean 'JVMs'?", "location": {"path": "articles/flow/advanced/signals.adoc", "range": {"start": {"line": 162, "column": 178}}}, "severity": "ERROR"}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably more confusing than helpful to mention clustering now since it's not yet supported. Either just drop the sentence or add some disclaimer about this being a planned feature but not yet implemented.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


== Usage Examples

=== Simple Counter Example

[source,java]
----
public class CounterView extends VerticalLayout {
private final NumberSignal counter =
SignalFactory.IN_MEMORY_SHARED.number("counter");

public CounterView() {
Button button = new Button("Click me",
click -> counter.incrementBy(1.0));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The decimals are just confusing here. incrementBy(1) is just as valid.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Span count = new Span();
Signal.effect(() -> count.setText("Count: " + counter.valueAsInt()));
add(button, count);
}
}
----

=== Form Binding Example

[source,java]
----
public class UserForm extends FormLayout {
private final ValueSignal<User> user =
SignalFactory.IN_MEMORY_SHARED.value("user", User.class);

public UserForm() {
TextField nameField = new TextField("Name");
NumberField ageField = new NumberField("Age");

Signal.effect(() -> {
User currentUser = user.value();
nameField.setValue(currentUser.getName());
ageField.setValue(currentUser.getAge());
});

nameField.addValueChangeListener(event ->
user.update(u -> new User(event.getValue(), u.getAge())));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This only works by accident due to the fact that update accidentally doesn't check against making changes from inside an effect.

Could instead show a simpler example with just a single share text field and avoid the change-from-effect restriction by updating the signal only if !event.getValue().equals(signal.peek()).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


ageField.addValueChangeListener(event ->
user.update(u -> new User(u.getName(), event.getValue())));

add(nameField, ageField);
}
}
----

=== List Example
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on experience, users are likely to ask how to render components for each child signal. We could show an example for that along the lines of this:

ComponentEffect.effect(this, () -> {
  wrapper.removeAll();
  names.value().forEach(nameSignal -> {
    Span span = new Span();
    ComponentEffect.bind(span, nameSignal, Span::setText);
    wrapper.add(span);
  });
});

Along with a note that we plan to provide a more efficient helper later.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


[source,java]
----
public class PersonList extends VerticalLayout {
private final ListSignal<Person> persons =
SignalFactory.IN_MEMORY_SHARED.list("persons", Person.class);

public PersonList() {
Grid<Person> grid = new Grid<>();
grid.addColumn(Person::getName).setHeader("Name");
grid.addColumn(Person::getAge).setHeader("Age");

Button addButton = new Button("Add Person", click -> {
persons.insertLast(new Person("New Person", 25));
});

Signal.effect(() -> {
List<Person> list = persons.value().stream()
.map(Signal::value)
.toList();
grid.setItems(list);
});

add(grid, addButton);
}
}
----

=== Node Example
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems overly complex and could maybe be dropped

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


[source,java]
----
public class CategoryForm extends FormLayout {
private final NodeSignal category =
SignalFactory.IN_MEMORY_SHARED.node("category");

public CategoryForm() {
TextField nameField = new TextField("Name");
NumberField idField = new NumberField("ID");

Signal.effect(() -> {
nameField.setValue(category.value().mapChildren()
.get("name").asValue(String.class).value());
idField.setValue(category.value().mapChildren()
.get("id").asValue(Double.class).value());
});

nameField.addValueChangeListener(event -> {
// ignores nameField.setValue() calls from the server to avoid infinite loop
if (event.isFromClient()) {
ValueSignal<String> nameSignal = category.value().mapChildren()
.get("name").asValue(String.class);
nameSignal.value(event.getValue());
}
});

idField.addValueChangeListener(event -> {
// ignores idSignal.setValue() calls from the server to avoid infinite loop
if (event.isFromClient()) {
ValueSignal<Double> idSignal = category.value().mapChildren()
.get("id").asValue(Double.class);
idSignal.value(event.getValue());
}
});

add(nameField, idField);
}
}
----

== 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,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 Effects for UI Updates

Use [methodname]`Signal.effect()` to automatically update the UI when signal values change.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should not show any example that updates UI with a "regular" effect rather than using ComponentEffect.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


[source,java]
----
Signal.effect(() -> {
label.setText("Hello, " + user.value().getName());
});
----

=== Use Transactions for Atomic Updates

Use transactions when you need to update multiple signals atomically.

[source,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,java]
----
counter.update(current -> current + 1);
----

=== Cleanup Effects When No Longer Needed

Store the returned [classname]`Runnable` from [methodname]`Signal.effect()` and call it to clean up the effect when it's no longer necessary.

[source,java]
----
Runnable cleanup = Signal.effect(() -> {
// Update UI
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same about avoiding examples that "use" effect together with anything related to the UI

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

});

// Later, when the effect is no longer needed
cleanup.run();
----

== Advanced Topics

=== Computed Signals
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't seem like this belongs in the "advanced" section since it shows exactly the same thing that was already shown in the very beginning of the page.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


Computed signals derive their values from other signals and automatically update when those signals change.

[source,java]
----
ValueSignal<String> firstName = SignalFactory.IN_MEMORY_SHARED.value("firstName", String.class);
ValueSignal<String> lastName = SignalFactory.IN_MEMORY_SHARED.value("lastName", String.class);

Signal<String> fullName = Signal.computed(() ->
firstName.value() + " " + lastName.value());
----

=== Signal Mapping

You can transform a signal's value using the map() method.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could mention that this is just a shorthand for creating a computed signal

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


[source,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.

[source,java]
----
ValueSignal<String> name = SignalFactory.IN_MEMORY_SHARED.value("name", String.class);
ValueSignal<String> readOnlyName = name.asReadonly();
----
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could clarify that changes can still be made through name and that such changes will also be visible through readOnlyName

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


=== Untracked Signal Access

Check failure on line 384 in articles/flow/advanced/signals.adoc

View workflow job for this annotation

GitHub Actions / lint

[vale] reported by reviewdog 🐶 [Vale.Spelling] Did you really mean 'Untracked'? Raw Output: {"message": "[Vale.Spelling] Did you really mean 'Untracked'?", "location": {"path": "articles/flow/advanced/signals.adoc", "range": {"start": {"line": 384, "column": 5}}}, "severity": "ERROR"}

You can access a signal's value without registering a dependency, i.e., without triggering reactive effect functions.
It's also possible to pick the confirmed value of a signal without triggering the effect function.

[source,java]
----
String name = nameSignal.peek(); // Won't trigger Signal.effect()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be easier to understand how this works if the code would be inside an actual effect callback.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

String name = nameSignal.peekConfirmed(); // Also won't trigger Signal.effect() and returns only the confirmed value
----
Loading