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.

Note
Preview Feature

This is a preview version of Signals. You need to enable it with the feature flag com.vaadin.experimental.flowFullstackSignals. Preview versions may lack some planned features, and breaking changes may be introduced in any Vaadin version. We encourage you to try it out and provide feedback to help us improve it.

Overview

The ValueSignal class is a writable signal that holds a reference to a value. Local signals are scoped to a single UI session, providing a simple and efficient way to manage component-level state.

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

ValueSignal<String> localName = new ValueSignal<>("Initial value");

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 value() method to both read and write signal values:

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

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

// Write a new value
signal.value("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));

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.value();
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);

        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 cannot participate in signal transactions:

Source code
Java
ValueSignal<String> local = new ValueSignal<>("value");

// This will throw an exception
Signal.runInTransaction(() -> {
    local.value("new value"); // 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");

ComponentEffect.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.value() 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 the two-way mapping variants.

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(getter, merger):

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));

// Create a two-way mapping to the 'done' property
WritableSignal<Boolean> doneSignal = todoSignal.map(Todo::done, Todo::withDone);

// Bind checkbox directly to the property
Checkbox checkbox = new Checkbox();
checkbox.bindValue(doneSignal);
// Checking the box updates the todo's done property
// Calling todoSignal.update(...) updates the checkbox

The merger function 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 mapMutable(getter, modifier):

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());

// Create a two-way mapping to the 'name' property
WritableSignal<String> nameSignal = userSignal.mapMutable(User::getName, User::setName);

// Bind text field directly to the property
TextField nameField = new TextField("Name");
nameField.bindValue(nameSignal);

The modifier function 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 mapMutable() only when working with existing mutable bean classes.

Comparison: Read-Only vs Two-Way Mapping

Method Use Case Return Type

map(getter)

Read-only transformations

Signal<T> (read-only)

map(getter, merger)

Two-way binding with immutable values

WritableSignal<T>

mapMutable(getter, modifier)

Two-way binding with mutable beans

WritableSignal<T>

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:

  • 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);
    }
}