-
Notifications
You must be signed in to change notification settings - Fork 209
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
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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
|
||
description: Using the Flow Signals library to manage UI state reactively in Vaadin applications. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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". There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
|
||
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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
|
||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
|
||
|
||
=== 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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could emphasize that dependency tracking is automatic. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done |
||
button.setText("Counter: " + counter.value()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should really use There was a problem hiding this comment. Choose a reason for hiding this commentThe 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(() -> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could make sense to show the constructor here since There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
---- | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could also show how to modify the value, i.e. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Doesn't compile. The second parameter should be a There was a problem hiding this comment. Choose a reason for hiding this commentThe 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' | ||
---- | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could also show the alternative way of accessing data using an There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
|
||
|
||
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
|
||
|
||
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
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The decimals are just confusing here. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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()))); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This only works by accident due to the fact that 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
Along with a note that we plan to provide a more efficient helper later. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems overly complex and could maybe be dropped There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should not show any example that updates UI with a "regular" There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same about avoiding examples that "use" There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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(); | ||
---- | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could clarify that changes can still be made through There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
|
||
|
||
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() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
---- |
There was a problem hiding this comment.
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".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done