Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
140 changes: 130 additions & 10 deletions articles/flow/ui-state/building-ui.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -77,15 +77,15 @@
// Automatically updates to "Jane Doe"
----

For expensive computations, wrap the lambda in [methodname]`Signal.computed()` to cache the result:
For expensive computations, wrap the computation in [methodname]`Signal.cached()` to cache the result:

[source,java]
----
fullName.bindText(Signal.computed(() ->
firstName.get() + " " + lastName.get()));
fullName.bindText(Signal.cached(Signal.computed(() ->
firstName.get() + " " + lastName.get())));
----

Both variants execute the callback when a dependency signal value changes. The difference is that the lambda variant recalculates the result every time someone reads the lambda signal value, while an explicit computed signal (using [methodname]`Signal.computed()`) caches the result until any dependency signal value changes. For simple operations like [methodname]`String::length`, a lambda is enough. However, for complex or expensive computations, use an explicit computed signal to avoid unnecessary recalculations when the value is read multiple times.
Both variants execute the callback when a dependency signal value changes. The difference is that a plain lambda or [methodname]`Signal.computed()` signal recalculates the result every time someone reads the signal value, while a cached signal (using [methodname]`Signal.cached()`) stores the result and reuses it until any dependency signal value changes. For simple operations like [methodname]`String::length`, a lambda is enough. However, for complex or expensive computations, use [methodname]`Signal.cached()` to avoid unnecessary recalculations when the value is read multiple times. See <<effects-computed#caching-with-signal-cached,Caching with Signal.cached()>> for details.


[TIP]
Expand Down Expand Up @@ -567,6 +567,110 @@
Use [methodname]`bindChildren()` when you want to create custom components for each item in a layout container (e.g., [classname]`VerticalLayout`). Use [methodname]`Signal.effect()` with [methodname]`setItems()` when you want to populate a data items component (e.g., [classname]`Grid`, [classname]`VirtualList`) that manages its own rendering.


[role="since:com.vaadin:vaadin@V25.2"]
== Component-Specific Bindings

In addition to the general-purpose bindings, some components provide binding methods for state that is specific to that component. Like the other binding methods, they return a [classname]`SignalBinding` that can be used to register `onChange` callbacks or to unbind. Two-way bindings accept a write callback as the second argument; pass `null` for one-way binding.


=== Grid Selection Binding

Bind the selection of a [classname]`Grid` to a signal through the single-select or multi-select wrapper. Use [methodname]`asSingleSelect().bindValue()` to bind the selected item:

[source,java]
----
ValueSignal<Person> selectedPerson = new ValueSignal<>();

Grid<Person> grid = new Grid<>(Person.class);
grid.asSingleSelect().bindValue(selectedPerson, selectedPerson::set);

// Selecting a row updates the signal;
// setting the signal updates the selection
----

Use [methodname]`asMultiSelect().bindValue()` to bind the set of selected items to a `Signal<Set<T>>`:

[source,java]
----
ValueSignal<Set<Person>> selectedPersons = new ValueSignal<>(Set.of());

Grid<Person> grid = new Grid<>(Person.class);
grid.setSelectionMode(Grid.SelectionMode.MULTI);
grid.asMultiSelect().bindValue(selectedPersons, selectedPersons::set);
----


=== MessageList Items Binding

Use [methodname]`bindItems()` to bind the items of a [classname]`MessageList` to a signal holding a list of item signals, such as a [classname]`ListSignal<MessageListItem>`. The rendered messages update when the list structure or any individual item signal changes:

[source,java]
----
ListSignal<MessageListItem> messages = new ListSignal<>();

MessageList messageList = new MessageList();
messageList.bindItems(messages);

messages.insertLast(new MessageListItem("Hello!", Instant.now(), "Alice"));
// The new message appears in the list
----

As a shorthand, [classname]`MessageList` also has a constructor that binds the items signal directly:

[source,java]
----
MessageList messageList = new MessageList(messages);
----

While the binding is active, modifying the items manually through [methodname]`setItems()` or [methodname]`addItem()` throws a [classname]`BindingActiveException`.


=== AppLayout Drawer Binding

Use [methodname]`bindDrawerOpened()` to bind the drawer state of an [classname]`AppLayout` to a boolean signal:

[source,java]
----
ValueSignal<Boolean> drawerOpened = new ValueSignal<>(true);

AppLayout appLayout = new AppLayout();

Check failure on line 636 in articles/flow/ui-state/building-ui.adoc

View workflow job for this annotation

GitHub Actions / lint

[vale] reported by reviewdog 🐶 [Vaadin.ProductName] Use 'App Layout' instead of 'AppLayout'. Raw Output: {"message": "[Vaadin.ProductName] Use 'App Layout' instead of 'AppLayout'.", "location": {"path": "articles/flow/ui-state/building-ui.adoc", "range": {"start": {"line": 636, "column": 1}}}, "severity": "ERROR"}
appLayout.bindDrawerOpened(drawerOpened, drawerOpened::set);

// Toggling the drawer in the browser updates the signal;
// setting the signal opens or closes the drawer
----


=== Checkbox Indeterminate Binding

Use [methodname]`bindIndeterminate()` to bind the indeterminate state of a [classname]`Checkbox` to a boolean signal. A typical use case is a "Select All" checkbox indicating that some, but not all, items are selected:

[source,java]
----
ValueSignal<Boolean> indeterminate = new ValueSignal<>(true);

Checkbox selectAll = new Checkbox("Select all");
selectAll.bindIndeterminate(indeterminate, indeterminate::set);
----


=== Theme Variant Binding

Use [methodname]`bindThemeVariants()` to bind a list of theme variants to a signal. This method is available on components that implement [interfacename]`HasThemeVariant` and is a typed alternative to [methodname]`bindThemeNames()` that works with theme variant enums instead of raw theme name strings:

[source,java]
----
ValueSignal<List<ButtonVariant>> variants =
new ValueSignal<>(List.of(ButtonVariant.LUMO_PRIMARY));

Button saveButton = new Button("Save");
saveButton.bindThemeVariants(variants);

// Update all theme variants at once
variants.set(List.of(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_SMALL));
----


== Built-In Signal Sources

Vaadin provides several built-in signals that expose framework-level state reactively.
Expand Down Expand Up @@ -595,28 +699,44 @@

=== UI Locale Signal

Use [methodname]`UI.localeSignal()` to get a writable signal that tracks the UI's locale:
Use [methodname]`UI.localeSignal()` to get a read-only signal that tracks the UI's locale:

[source,java]
----
ValueSignal<Locale> locale = UI.getCurrent().localeSignal();
Signal<Locale> locale = UI.getCurrent().localeSignal();

Span greeting = new Span();
greeting.bindText(locale.map(loc ->
loc.getLanguage().equals("fi") ? "Tervetuloa" : "Welcome"));
----

[WARNING]
Writing directly to this signal does not notify [interfacename]`LocaleChangeObserver` implementations. If you need observers to be notified, use [methodname]`UI.setLocale()` instead.
The signal cannot be written directly. To change the locale, use [methodname]`UI.setLocale()`, which updates the signal and also notifies [interfacename]`LocaleChangeObserver` implementations. For two-way binding with a field component, pass [methodname]`setLocale()` as the write callback:

[source,java]
----
UI ui = UI.getCurrent();

ComboBox<Locale> languageSelect = new ComboBox<>("Language");
languageSelect.setItems(Locale.ENGLISH, Locale.of("fi"));
languageSelect.bindValue(ui.localeSignal(), ui::setLocale);
----


=== Session Locale Signal

Use [methodname]`VaadinSession.localeSignal()` to get a shared, writable signal that tracks the session-level locale. UIs that have not had their locale explicitly overridden derive their locale from this signal:
Use [methodname]`VaadinSession.localeSignal()` to get a read-only signal that tracks the session-level locale. UIs that have not had their locale explicitly overridden derive their locale from this signal:

[source,java]
----
VaadinSession session = VaadinSession.getCurrent();
Signal<Locale> sessionLocale = session.localeSignal();
----

To change the session locale, use [methodname]`VaadinSession.setLocale()`. As with the UI locale signal, the signal can be combined with [methodname]`setLocale()` as the write callback for two-way binding:

[source,java]
----
SharedValueSignal<Locale> sessionLocale = VaadinSession.getCurrent().localeSignal();
languageSelect.bindValue(session.localeSignal(), session::setLocale);
----


Expand Down
29 changes: 19 additions & 10 deletions articles/flow/ui-state/effects-computed.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ Signal<String> fullName = Signal.computed(() -> {
});
----

Computed signals are read-only; you cannot set their value directly.
Computed signals are read-only; you cannot set their value directly. The computation callback runs every time the signal value is read. For expensive computations, wrap the computed signal in [methodname]`Signal.cached()` to avoid repeating the work on every read (see <<#caching-with-signal-cached,Caching with Signal.cached()>>).


=== Combining Multiple Signals
Expand All @@ -136,22 +136,31 @@ quantity.set(3.0);
----


=== Caching and Lazy Evaluation
[[caching-with-signal-cached]]
[role="since:com.vaadin:vaadin@V25.2"]
=== Caching with Signal.cached()

Computed signals cache their value and only recalculate when dependencies change:
A computed signal created with [methodname]`Signal.computed()` doesn't cache its value: the computation callback runs every time the value is read. This keeps the signal lightweight, but reading the value of an expensive computation from several places repeats the work.

Use [methodname]`Signal.cached()` to add caching on top of a computed signal. A cached signal stores the most recently computed value and returns it on subsequent reads. The cached value remains valid until the value of any dependency signal changes:

[source,java]
----
Signal<Integer> expensiveComputation = Signal.computed(() -> {
// This only runs when dependencies change
return performExpensiveCalculation(inputSignal.get());
});
Signal<Integer> expensiveComputation = Signal.cached(Signal.computed(() ->
performExpensiveCalculation(inputSignal.get())));

expensiveComputation.get(); // Computes the value
expensiveComputation.get(); // Returns the cached value

// Multiple reads return the cached value
expensiveComputation.get(); // Computes once
expensiveComputation.get(); // Returns cached value
inputSignal.set(newInput);
expensiveComputation.get(); // Recomputes because a dependency changed
----

Use plain [methodname]`Signal.computed()` for cheap derivations such as string concatenation or simple arithmetic, where the bookkeeping overhead of caching would outweigh the cost of recomputing the value. Use [methodname]`Signal.cached()` when the computation is expensive — for example, filtering or sorting a large list — and the value is read multiple times.

[NOTE]
An effect or an outer cached signal that uses the value from a cached signal isn't re-run when the inner computation is invalidated but produces the same value as before. This makes [methodname]`Signal.cached()` useful for cutting off update chains when an expensive intermediate result is unchanged.


== Signal Mapping

Expand Down
23 changes: 23 additions & 0 deletions articles/flow/ui-state/local-signals.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,29 @@ ValueSignal<String> middleItem = items.insertAt(1, "Middle");
Each insert method returns a [classname]`ValueSignal<T>` representing the entry. You can use this signal to update or remove the entry later.


[role="since:com.vaadin:vaadin@V25.2"]
=== Adding Multiple Items

Use [methodname]`insertAllFirst()`, [methodname]`insertAllLast()`, or [methodname]`insertAllAt()` to insert several items in one operation. All entries are added with a single change notification, which is more efficient than inserting items one by one:

[source,java]
----
ListSignal<String> items = new ListSignal<>();
items.insertLast("Existing");

// Insert at the end, preserving the collection order
List<ValueSignal<String>> added = items.insertAllLast(List.of("A", "B"));

// Insert at the beginning
items.insertAllFirst(List.of("First", "Second"));

// Insert at a specific index (0-indexed)
items.insertAllAt(1, List.of("X", "Y"));
----

Each method returns an unmodifiable list of [classname]`ValueSignal` instances for the inserted entries, in the same order as the provided collection.


=== Removing Items

Use [methodname]`remove()` to remove a specific entry, or [methodname]`clear()` to remove all entries:
Expand Down
26 changes: 26 additions & 0 deletions articles/flow/ui-state/shared-signals.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,32 @@ items.insertAt("New Last", ListPosition.last());
----


[role="since:com.vaadin:vaadin@V25.2"]
==== Inserting Multiple Items

Use [methodname]`insertAllFirst()`, [methodname]`insertAllLast()`, or [methodname]`insertAllAt()` to insert several items in one operation. All inserts are performed within a single transaction, so other users see the entire batch as one atomic change and only one synchronization is needed:

[source,java]
----
import com.vaadin.flow.signals.operations.BulkInsertOperation;

SharedListSignal<String> items = new SharedListSignal<>(String.class);

// Insert at the end, preserving the collection order
BulkInsertOperation<SharedValueSignal<String>> operation =
items.insertAllLast(List.of("A", "B", "C"));

// Insert at the beginning
items.insertAllFirst(List.of("First", "Second"));

// Insert at a specific position
items.insertAllAt(List.of("X", "Y"),
ListPosition.after(operation.signals().get(0)));
----

Each method returns a [classname]`BulkInsertOperation` that provides the signals for the inserted entries through [methodname]`signals()` and tracks the entire batch with a single result future through [methodname]`result()`. The signals can be used immediately, even before the result of the operation is confirmed.


==== Reordering Items with moveTo

Use [methodname]`moveTo()` to change the position of an existing list entry without removing and re-inserting it:
Expand Down
Loading