Docs

Documentation versions (currently viewingVaadin 25.1 (pre-release))

Local Signals

Using local signals for UI-only state management.

Local signals provide lightweight, UI-only state management for scenarios where state doesn’t need to be shared across users or sessions. They are ideal for managing component-level state like visibility toggles, form state, or local UI preferences.

Overview

Local signals are scoped to a single UI session, providing a simple and efficient way to manage component-level state. Two types of local signals are available:

  • ValueSignal<T> - holds a single value

  • ListSignal<T> - holds an ordered list of values with per-entry reactivity

Source code
Java
import com.vaadin.flow.signals.local.ValueSignal;
import com.vaadin.flow.signals.local.ListSignal;

ValueSignal<String> name = new ValueSignal<>("Initial value");
ListSignal<String> items = new ListSignal<>();

Creating Local Signals

Local signals can be created with or without an initial value:

Source code
Java
// With initial value
ValueSignal<String> nameSignal = new ValueSignal<>("John");

// Without initial value (null)
ValueSignal<User> userSignal = new ValueSignal<>();

Reading and Writing Values

Use the get() method to read signal values and set() to write them:

Source code
Java
ValueSignal<String> signal = new ValueSignal<>("Hello");

// Read the current value
String current = signal.get();

// Write a new value
signal.set("World");

Updating Values

The update() method allows you to update a value based on its current state:

Source code
Java
ValueSignal<Integer> counter = new ValueSignal<>(0);

// Update based on current value
counter.update(current -> current + 1);

This is particularly useful for updating records or beans where you need to create a new instance with modified properties:

Source code
Java
record Task(String text, TaskState state) {}

ValueSignal<Task> taskSignal = new ValueSignal<>(new Task("Write docs", TaskState.PENDING));

// Update task state by creating a new record with the modified value
taskSignal.update(t -> new Task(t.text(), TaskState.COMPLETED));

ListSignal

ListSignal<T> provides an ordered list of values where each entry is independently reactive. Changes to the list structure (adding or removing items) trigger list-level dependents, while changes to individual entry values only trigger that entry’s dependents.

Source code
Java
import com.vaadin.flow.signals.local.ListSignal;

ListSignal<String> tags = new ListSignal<>();

Adding Items

Use insertFirst(), insertLast(), or insertAt() to add items:

Source code
Java
ListSignal<String> items = new ListSignal<>();

// Add to the end (most common)
ValueSignal<String> lastItem = items.insertLast("Last");

// Add to the beginning
ValueSignal<String> firstItem = items.insertFirst("First");

// Add at a specific position (0-indexed)
ValueSignal<String> middleItem = items.insertAt(1, "Middle");

// List is now: ["First", "Middle", "Last"]

Each insert method returns a ValueSignal<T> representing the entry. You can use this signal to update or remove the entry later.

Removing Items

Use remove() to remove a specific entry, or clear() to remove all entries:

Source code
Java
ListSignal<String> items = new ListSignal<>();
ValueSignal<String> entry = items.insertLast("Item to remove");

// Remove a specific entry
items.remove(entry);

// Remove all entries
items.clear();

Reading List Contents

Use get() to get a snapshot of the list:

Source code
Java
ListSignal<String> items = new ListSignal<>();
items.insertLast("A");
items.insertLast("B");

// Get a snapshot list of ValueSignal entries
List<ValueSignal<String>> entries = items.get();

// Read values from entries
entries.forEach(entry -> System.out.println(entry.get()));

Updating Entry Values

Since each list entry is a ValueSignal, you can update individual entries without affecting other entries or the list structure:

Source code
Java
ListSignal<String> items = new ListSignal<>();
ValueSignal<String> entry = items.insertLast("Original");

// Update the entry value
entry.set("Updated");

// The list still has one entry, but its value changed

This per-entry reactivity is efficient: only components bound to the changed entry update, not all components bound to the list.

ListSignal Example

Here’s an example of dynamic phone number fields where users can add and remove entries:

Source code
Java
public class PhoneNumbersEditor extends VerticalLayout {
    private final ListSignal<String> phoneNumbers = new ListSignal<>();

    public PhoneNumbersEditor() {
        VerticalLayout phoneList = new VerticalLayout();
        phoneList.setPadding(false);
        phoneList.setSpacing(false);

        phoneList.bindChildren(phoneNumbers, phoneSignal -> {
            HorizontalLayout row = new HorizontalLayout();
            row.setAlignItems(FlexComponent.Alignment.CENTER);

            TextField phoneField = new TextField();
            phoneField.setPlaceholder("Phone number");
            phoneField.bindValue(phoneSignal, phoneSignal::set);

            Button removeButton = new Button(VaadinIcon.MINUS.create());
            removeButton.addClickListener(e -> phoneNumbers.remove(phoneSignal));

            row.add(phoneField, removeButton);
            return row;
        });

        Button addButton = new Button("Add phone number", e -> phoneNumbers.insertLast(""));
        add(phoneList, addButton);
    }
}

When to Use ListSignal vs SharedListSignal

Use ListSignal when:

  • The list is only relevant to a single user’s UI session

  • You don’t need to synchronize the list across users or browser tabs

  • Examples: dynamic form fields, local shopping cart UI, tabs/panels, temporary selections

Use SharedListSignal when:

  • Multiple users need to see the same list in real-time

  • You need transactional guarantees for list operations

  • Examples: collaborative todo lists, shared document comments, live dashboards

Replacing Values

The replace() method performs a compare-and-set operation:

Source code
Java
ValueSignal<String> status = new ValueSignal<>("pending");

// Only replaces if current value matches expected
boolean replaced = status.replace("pending", "complete");

Working with Mutable Values

For mutable values, use the modify(Consumer) method to perform updates. This method ensures the signal properly tracks changes:

Source code
Java
public class User {
    private String name;
    private int age;

    // getters and setters...
}

ValueSignal<User> userSignal = new ValueSignal<>(new User("Jane", 25));

// Correct: Use modify() for mutable objects
userSignal.modify(user -> user.setAge(26));

// Incorrect: Direct mutation won't trigger reactivity
User user = userSignal.get();
user.setAge(27); // This change won't be detected!
Warning
Direct mutation of objects retrieved from a signal won’t trigger reactive updates. Always use modify() for mutable values, or preferably use immutable types like Java records.

Thread Safety

Local signals are thread-safe and can be updated from any thread without wrapping calls in ui.access(). The signal handles UI synchronization internally:

Source code
Java
public class MyView extends VerticalLayout {
    private final ValueSignal<String> localState = new ValueSignal<>("");

    public MyView() {
        TextField field = new TextField();
        field.bindValue(localState, localState::set);

        Span output = new Span();
        output.bindText(localState);
        add(field, output);
    }
}
Note
When updating signals from background threads, ensure your application has the @Push annotation enabled. This allows the server to push UI updates to the client when signal values change asynchronously.

However, local signals are scoped to a single server instance. Vaadin doesn’t yet provide an integrated solution for sharing local signals within a cluster. If you need state that works across a cluster or is shared between users, use Shared Signals instead.

Transaction Limitations

Local signals (ValueSignal and ListSignal) cannot participate in signal transactions:

Source code
Java
ValueSignal<String> localValue = new ValueSignal<>("value");
ListSignal<String> localList = new ListSignal<>();

// This will throw an exception
Signal.runInTransaction(() -> {
    localValue.set("new value"); // Not allowed!
    localList.insertLast("item");  // Not allowed!
});

Use shared signals if you need transactional guarantees. See Transactions for more information about signal transactions.

Peeking Values

Use peek() to read a value without registering a dependency in effects or computed signals:

Source code
Java
ValueSignal<String> signal = new ValueSignal<>("Hello");

Signal.effect(component, () -> {
    // peek() doesn't register a dependency
    String peeked = signal.peek();
    // This effect won't re-run when signal changes
});

Transforming Signal Values

Use the map() method to create a computed signal that transforms the original value:

Source code
Java
ValueSignal<String> name = new ValueSignal<>("john");
Signal<String> upperName = name.map(String::toUpperCase);

// upperName.get() returns "JOHN"
// When name changes, upperName automatically updates

Use map() for single-signal transformations. For transformations depending on multiple signals, use Signal.computed() instead.

Two-Way Signal Mapping

The read-only map() method described above creates a derived signal that updates when the source changes, but changes to the derived signal don’t propagate back. For two-way binding scenarios where you need changes to flow in both directions, use bindValue() with separate getter and setter arguments.

This is particularly useful when binding a form field to a property of a complex object stored in a signal. For example, binding a checkbox directly to the done property of a Todo record.

Mapping Immutable Values (Records)

For immutable values like Java records, use map() for reading and updater() for writing:

Source code
Java
record Todo(String text, boolean done) {
    Todo withDone(boolean done) {
        return new Todo(this.text, done);
    }
}

ValueSignal<Todo> todoSignal = new ValueSignal<>(new Todo("Write docs", false));

// Bind checkbox directly to the 'done' property
Checkbox checkbox = new Checkbox();
checkbox.bindValue(todoSignal.map(Todo::done), todoSignal.updater(Todo::withDone));
// Checking the box updates the todo's done property
// Calling todoSignal.update(...) updates the checkbox

The updater() method takes a wither function that receives the current parent value and the new child value, and returns a new parent value with the child property updated. Using wither methods like withDone() is a common pattern for records.

Mapping Mutable Values (Beans)

For mutable beans with setters, use map() for reading and modifier() for writing:

Source code
Java
public class User {
    private String name;
    private int age;

    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public int getAge() { return age; }
    public void setAge(int age) { this.age = age; }
}

ValueSignal<User> userSignal = new ValueSignal<>(new User());

// Bind text field directly to the 'name' property
TextField nameField = new TextField("Name");
nameField.bindValue(userSignal.map(User::getName), userSignal.modifier(User::setName));

The modifier() method takes a setter function that receives the parent object and the new value, and mutates the parent in place. The signal framework handles change notification automatically.

Note

Prefer immutable records over mutable beans when possible. Records are easier to reason about, naturally thread-safe, and work better with reactive patterns. Use modifier() only when working with existing mutable bean classes.

Comparison: Read-Only vs Two-Way Mapping

Method Use Case

map(getter)

Read-only transformations

bindValue(signal.map(getter), signal.updater(merger))

Two-way binding with immutable values

bindValue(signal.map(getter), signal.modifier(setter))

Two-way binding with mutable beans

Organizing Signals as Fields

Storing signals as class fields is recommended for better code organization. It keeps all reactive state together at the top of the class, making it easier to understand the component’s state at a glance:

Source code
Java
public class MyView extends VerticalLayout {
    // State is clearly visible at the top of the class
    private final ValueSignal<String> nameSignal = new ValueSignal<>("");
    private final Signal<String> greeting = nameSignal.map(n -> "Hello, " + n);

    public MyView() {
        // Components can be local variables since they're added to the layout
        Span greetingSpan = new Span();
        greetingSpan.bindText(greeting);
        add(greetingSpan);
    }
}

This pattern also allows computed signals to be defined declaratively alongside their source signals, making the reactive dependencies clear.

Use Cases

Local signals are ideal for:

ValueSignal use cases:

  • UI toggle states: Panel expansion, modal visibility, sidebar open/closed

  • Form state: Current step in a wizard, validation state

  • Local filters: Search text, sort order within a single view

  • Temporary state: Editing mode, selection state

Source code
Java
public class ExpandablePanel extends VerticalLayout {
    private final ValueSignal<Boolean> expanded = new ValueSignal<>(false);

    public ExpandablePanel(String title, Component content) {
        Button header = new Button(title);
        header.addClickListener(e -> expanded.update(v -> !v));

        // Reactively show/hide content
        content.bindVisible(expanded);

        add(header, content);
    }
}

ListSignal use cases:

  • Dynamic form fields: Add/remove phone numbers, addresses, or other repeating fields

  • Local shopping cart: Managing cart items in a single-user session

  • UI tabs or panels: Dynamic tabs that users can open and close

  • Temporary selections: Multi-select items before batch operations