Skip to content

signals patterns reference

Stefan Uebe edited this page Mar 27, 2026 · 1 revision

Vaadin Signals — Idiomatic Patterns Reference

Compiled from the official vaadin/signals-cases repository (all 20+ use cases) and the Vaadin 25.2 documentation. Use this document when reviewing signal code for correctness, performance, and idiomatic style.


Table of Contents

  1. Taxonomy: Which Signal Type to Use
  2. get() vs peek() — The Most Misused Decision
  3. Effect Scoping and Lifecycle
  4. Split Effects — One Concern Per Effect
  5. Binding Methods — Prefer Over Manual Effects
  6. Computed Signals and map()
  7. Two-Way Mapping: updater() and modifier()
  8. bindChildren() for Dynamic Lists
  9. peek() to Avoid Circular Dependencies
  10. Service-to-Signal Pattern
  11. Event Bridge Pattern
  12. Computed Signal Chains
  13. Session-Scoped Signals
  14. Async Operations and Signals
  15. Debounce with Manual Scheduling
  16. Binder + Signal Integration
  17. Element.bindAttribute() for Non-Component DOM
  18. Signal.not() and Boolean Derivations
  19. Custom Equality Checkers
  20. Memory and Lifecycle: Standalone vs Component Effects
  21. Anti-Patterns Index

1. Taxonomy: Which Signal Type to Use

Scenario Signal Type
Single-user local UI state (toggle, form field, local filter) ValueSignal<T>
Dynamic list of items, single-user ListSignal<T>
Cross-user shared state (real-time collaboration, dashboards) SharedValueSignal<T> / SharedListSignal<T>
Derived/computed value from one source signal.map(fn)
Derived value from multiple sources Signal.computed(() -> ...)
Read-only exposure of a writable signal signal.asReadonly()
Negating a boolean signal Signal.not(signal)

Rule: ValueSignal / ListSignal cannot participate in transactions. If you need transactional atomicity (all-or-nothing updates), use shared signals.

Rule: Declare signals as final class fields, grouped at the top of the class. This makes the reactive state explicit.

// Good — state is visible at a glance
public class MyView extends VerticalLayout {
    private final ValueSignal<String> querySignal = new ValueSignal<>("");
    private final ValueSignal<Boolean> loadingSignal = new ValueSignal<>(false);
    private final Signal<Boolean> canSearch = querySignal.map(s -> !s.isBlank());

    public MyView() { ... }
}

2. get() vs peek()

This is the most important distinction and the source of most bugs.

Method Registers dependency? Where to use
get() YES Inside Signal.effect(), Signal.computed(), bindText() lambdas, any reactive context
peek() NO Click listeners, onAttach(), initialization code, service callbacks, debounce handlers, anywhere outside a reactive context

Hard rule: get() outside a reactive context throws IllegalStateException. peek() inside a reactive context silently breaks reactivity.

// CORRECT: get() in reactive context
Signal.effect(this, () -> {
    String value = nameSignal.get();   // dependency registered
    label.setText(value);
});

// CORRECT: peek() in event listener (outside reactive context)
button.addClickListener(e -> {
    String current = nameSignal.peek();  // no dependency, just a read
    doSomethingWith(current);
});

// CORRECT: peek() in service callback (background thread, not reactive context)
private void onDataUpdate(DashboardData data) {
    currentUsersSignal.set(data.currentUsers());
    // peek() used to read current window size when computing a new value
    if (timelineCategoriesSignal.peek().size() >= TIMELINE_POINTS) {
        timelineCategoriesSignal.remove(timelineCategoriesSignal.peek().getFirst());
    }
    timelineCategoriesSignal.insertLast(data.timestamp());
}

Use peek() for addToCart-style logic where you scan the list for an existing item without wanting to subscribe to every item change:

// UC06: Adding to cart — peek() to search without creating subscriptions
private void addToCart(Product product, ListSignal<CartItem> cartItemsSignal) {
    cartItemsSignal.peek().stream()
        .filter(signal -> signal.peek().product().id().equals(product.id()))
        .findFirst()
        .ifPresentOrElse(
            existing -> existing.set(existing.peek().withQuantity(existing.peek().quantity() + 1)),
            () -> cartItemsSignal.insertLast(new CartItem(product, 1)));
}

3. Effect Scoping and Lifecycle

Component-bound effects (most common)

Signal.effect(component, () -> { ... });
  • Active while component is attached to the DOM.
  • Automatically paused when component is detached; resumed on re-attach.
  • No manual cleanup needed.

Unbound/standalone effects

Registration cleanup = Signal.unboundEffect(() -> { ... });
// Must call cleanup.remove() when done — memory leak risk otherwise

Use only when the effect is not tied to any UI component lifetime (e.g., service-level background tracking). Always store the Registration and remove it explicitly.

EffectContext — initial run vs background change detection

Signal.effect(this, ctx -> {
    String value = priceSignal.get();
    span.setText("$" + value);

    if (!ctx.isInitialRun() && ctx.isBackgroundChange()) {
        span.getElement().executeJs("this.classList.add('highlight')");
    }
});

ctx.isInitialRun() — true on first execution. ctx.isBackgroundChange() — true when triggered by a different user session or background thread.

Manual effect removal

Registration effectRegistration = Signal.effect(chart, () -> { ... });
// Later:
effectRegistration.remove();

Useful when you need to detach an effect before the component is detached.


4. Split Effects

Rule: Each effect has exactly one responsibility. Never bundle multiple independent concerns into one large effect.

Why it matters: When a signal changes, only the effects that depend on it re-run. A fat effect re-runs on any of its many dependencies, doing unnecessary work.

// WRONG: One effect doing multiple things — ALL re-runs when ANY dependency changes
Signal.effect(container, () -> {
    nameLabel.setText(userSignal.get().name());
    ageLabel.setText(String.valueOf(userSignal.get().age()));
    statusIndicator.setVisible(userSignal.get().isActive());
    // ... more updates
});

// CORRECT: Separate bindings per concern — each updates only when its dependency changes
nameLabel.bindText(userSignal.map(User::name));
ageLabel.bindText(userSignal.map(u -> String.valueOf(u.age())));
statusIndicator.bindVisible(userSignal.map(User::isActive));

Real-world split from UC14 (dashboard with charts):

// Each series gets its own effect — only that series rerenders when its signal changes
bindChartData(chart, berlinSeries, berlinTimelineSignal);
bindChartData(chart, londonSeries, londonTimelineSignal);
bindChartData(chart, newYorkSeries, newYorkTimelineSignal);

// Separate effect for x-axis categories
Signal.effect(chart, () -> xAxis.setCategories(
    timelineCategoriesSignal.get().stream().map(Signal::get).toArray(String[]::new)));

// Separate "coordination" effect that only triggers chart.drawChart()
Signal.effect(chart, () -> {
    berlinTimelineSignal.get();    // register dependency
    londonTimelineSignal.get();    // register dependency
    newYorkTimelineSignal.get();   // register dependency
    chart.drawChart();
});

This approach: data effects update series independently; a single coordination effect fires drawChart() only after all series have had a chance to update.


5. Binding Methods

Rule: Use binding methods for standard component properties. Use Signal.effect() only for custom logic that binding methods cannot express.

Available binding methods on components

Method Target
bindText(signal) Text content (also: new Span(signal), new Paragraph(signal))
bindVisible(signal) Visibility
bindEnabled(signal) Enabled/disabled state
bindValue(readSignal, writeFn) Two-way form field binding
bindReadOnly(signal) Read-only state
bindRequiredIndicatorVisible(signal) Required indicator
bindClassName(name, booleanSignal) CSS class toggle
bindClassNames(listSignal) Full CSS class list
bindThemeName(name, booleanSignal) Theme variant toggle
bindThemeVariant(variant, booleanSignal) Button/component theme variant
bindHelperText(signal) Helper text
bindPlaceholder(signal) Placeholder text
bindWidth(signal) / bindHeight(signal) Size
getStyle().bind(property, signal) Inline CSS property
getThemeList().bind(name, booleanSignal) Theme class toggle
getElement().getClassList().bind(...) Element-level class

Multi-signal lambdas in binding methods

When a binding depends on multiple signals, pass a lambda instead of a signal directly:

// Depends on multiple signals — use lambda
section.bindVisible(() -> needsVisaSignal.get() && visaTypeSignal.get() == VisaType.H1B);

button.bindEnabled(() -> binder.validationStatusSignal().get().isOk()
    && submissionStateSignal.get() != SubmissionState.SUBMITTING);

The framework tracks which signals the lambda reads during execution and creates the appropriate dependencies dynamically.


6. Computed Signals and map()

map() — single-source derived values

ValueSignal<Integer> age = new ValueSignal<>(0);
Signal<String> category = age.map(a -> a < 18 ? "Child" : (a < 65 ? "Adult" : "Senior"));
Signal<Boolean> adult = age.map(a -> a >= 18);
  • Read-only.
  • Lazy: only recalculates when the source changes.
  • Cached: multiple reads return the cached value between changes.

Signal.computed() — multi-source derived values

Signal<Double> total = Signal.computed(() -> {
    double subtotal = price.get() * quantity.get();
    return subtotal * (1 + taxRate.get());
});
  • Use when combining two or more source signals.
  • Dependencies tracked dynamically — if a branch is not taken, signals in that branch are not dependencies.

Lambda vs Signal.computed() in bindText()

// Lambda: recalculates on every read AND on every dependency change
span.bindText(() -> firstName.get() + " " + lastName.get());

// Signal.computed(): caches the result, only recalculates on dependency change
span.bindText(Signal.computed(() -> firstName.get() + " " + lastName.get()));

For cheap string concat: lambda is fine. For expensive computations that might be read multiple times: use Signal.computed() explicitly.

Computed signal chains (UC06 shopping cart)

Computed signals can depend on other computed signals. The framework traces the full dependency graph automatically:

Signal<BigDecimal> subtotalSignal = Signal.computed(() ->
    cartItemsSignal.get().stream().map(ValueSignal::get)
        .map(item -> item.product().price().multiply(BigDecimal.valueOf(item.quantity())))
        .reduce(BigDecimal.ZERO, BigDecimal::add));

Signal<BigDecimal> discountSignal = Signal.computed(() -> {
    DiscountCode discount = validateDiscountCode(discountCodeSignal.get());
    if (discount != null) {
        return subtotalSignal.get().multiply(discount.percentage())    // depends on subtotalSignal
            .divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP);
    }
    return BigDecimal.ZERO;
});

Signal<BigDecimal> taxSignal = Signal.computed(() ->
    subtotalSignal.get().subtract(discountSignal.get())               // depends on both
        .multiply(new BigDecimal("0.08")).setScale(2, RoundingMode.HALF_UP));

Signal<BigDecimal> totalSignal = Signal.computed(() ->
    subtotalSignal.get().subtract(discountSignal.get())
        .add(shippingSignal.get()).add(taxSignal.get())
        .setScale(2, RoundingMode.HALF_UP));

When quantity changes in a cart item: subtotal → discount → tax → total all recalculate automatically. No manual wiring.

Dynamic dependency tracking (conditional dependencies)

Dependencies are tracked based on what was actually read in the last execution:

Signal.effect(component, () -> {
    if (showDetails.get()) {
        component.setText(details.get());   // showDetails + details are dependencies
    } else {
        component.setText(summary.get());   // only showDetails + summary are dependencies
    }
});

When showDetails is false, changes to details do NOT trigger re-execution. This is a performance optimization built into the tracking system.


7. Two-Way Mapping

For binding a form field directly to a property of a complex record or bean.

With immutable records — map() + updater()

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

Checkbox checkbox = new Checkbox();
checkbox.bindValue(
    todoSignal.map(Todo::done),             // read: Todo -> Boolean
    todoSignal.updater(Todo::withDone)      // write: (Todo, Boolean) -> Todo
);

updater(merger) receives the current parent value and the new child value, returns a new parent. The signal framework calls set() with the returned value automatically.

With mutable beans — map() + modifier()

TextField nameField = new TextField("Name");
nameField.bindValue(
    userSignal.map(User::getName),
    userSignal.modifier(User::setName)      // mutates in place, signal handles change notification
);

Prefer records over beans. Use modifier() only when working with existing mutable bean classes.

Nested mapping (UC22)

When the property lives inside a nested object:

Signal<Address> addressSignal = personSignal.map(Person::address);

cityField.bindValue(
    addressSignal.map(Address::city),
    personSignal.updater((person, city) ->
        person.withAddress(person.address().withCity(city)))
);

The updater always operates on the top-level signal (personSignal). The read side uses the intermediate addressSignal for clarity.

When NOT to use map()+updater()

Use Vaadin Binder instead when you need:

  • Field-level validation messages
  • Cross-field validation
  • isValid() checks
  • validationStatusSignal() for reactive valid/invalid state

Use map()+updater() for simple field-to-record binding without validation.


8. bindChildren() for Dynamic Lists

Rule: Use bindChildren() for dynamic component lists, never manually add/remove children in a loop.

ListSignal<CartItem> cartItemsSignal = new ListSignal<>();

VerticalLayout list = new VerticalLayout();
list.bindChildren(cartItemsSignal, itemSignal -> createCartItemRow(itemSignal));

What bindChildren() guarantees:

  • Factory function runs once per item (not on every list change)
  • Add: new component created, appended
  • Remove: component detached
  • Reorder: components moved in DOM, not recreated
  • Value update: existing component's bindings update, not recreated

Performance implication: If you use setItems() on a Grid or similar data component (which re-renders the entire list), that is NOT bindChildren(). For containers where you want individual components per item, bindChildren() is the correct API.

When to use Signal.effect() with setItems() instead:

// For Grid, ComboBox, VirtualList, etc. that manage their own rendering:
Signal.effect(grid, () -> grid.setItems(
    itemsSignal.get().stream().map(ValueSignal::get).toList()));

bindChildren() = custom component per item in a layout container. Signal.effect() + setItems() = data component (Grid, VirtualList) that renders its own rows.

UC15 trick — peek() inside bindChildren() factory when items are immutable:

// Items in this list are never updated after creation — peek() is correct
resultsContainer.bindChildren(searchResultsSignal,
    productSignal -> createProductCard(productSignal.peek()));

If items are never mutated after insertion, peek() in the factory saves subscription overhead. Only use this when you know items are immutable.


9. peek() to Avoid Circular Dependencies

The canonical example: a "change tracker" that stores both previous and current value.

record Change(double previous, double current) {}

ValueSignal<Change> changeSignal = new ValueSignal<>(
    new Change(signal.peek().doubleValue(), signal.peek().doubleValue()));

// Effect that tracks previous value — MUST use peek() on changeSignal
Signal.unboundEffect(() -> {
    double current = signal.get();                     // creates dependency on 'signal'
    double previous = changeSignal.peek().current();   // NO dependency on changeSignal
    changeSignal.set(new Change(previous, current));
});

If changeSignal.get() were used instead of peek(), the effect would depend on changeSignal. Since the effect also writes to changeSignal, it would create an infinite re-run loop. peek() breaks the cycle by reading the value without registering the dependency.

Pattern: When an effect needs to read a signal that it also writes to, use peek() for the read.


10. Service-to-Signal Pattern

Rule: Backend services must not know about signals or UI components. Callbacks are the boundary. Callbacks only update signals — they never manipulate UI directly.

// CORRECT architecture (UC14, real-time dashboard)

// Service: works with plain data, calls callbacks
public class SchedulerService {
    public void scheduleDashboardDataUpdate(Consumer<DashboardData> callback) { ... }
}

// View: registers callback, callback ONLY updates signals
public class DashboardView extends VerticalLayout {
    private final ValueSignal<Number> usersSignal = new ValueSignal<>(0);

    public DashboardView(SchedulerService service) {
        service.scheduleDashboardDataUpdate(this::onDataUpdate);
        // ... build UI bound to signals
    }

    // Callback: signals ONLY — no UI calls
    private void onDataUpdate(DashboardData data) {
        usersSignal.set(data.currentUsers());       // OK
        // label.setText(...) — FORBIDDEN here
    }
}

No UI.access() needed: Local signals (ValueSignal, ListSignal) are thread-safe and handle UI synchronization internally. You can call .set() from any thread. Push (@Push) must still be enabled for real-time delivery to the browser.


11. Event Bridge Pattern

Rule: Wrap any event-based API into a signal using event → signal.set(). The framework provides no magic here — it's always a manual adapter.

// Bridge 1: Upload callbacks → ValueSignal
Upload upload = new Upload(UploadHandler.inMemory(...)
    .whenStart(ctx -> uploadStateSignal.set(new UploadState.InProgress(...)))
    .onProgress((ctx, transferred, total) -> uploadStateSignal.set(...))
    .whenComplete((ctx, success) -> { if (!success) uploadStateSignal.set(new UploadState.Failed(...)); }));

// Bridge 2: Keyboard shortcuts → ValueSignal
Shortcuts.addShortcutListener(this, () -> lastShortcutSignal.set("Ctrl+K"), Key.KEY_K, KeyModifier.CONTROL);

// Bridge 3: Browser matchMedia → ValueSignal (requires @ClientCallable)
getElement().executeJs("""
    const mq = window.matchMedia('(prefers-color-scheme: dark)');
    mq.addEventListener('change', (e) => this.$server.onDarkModeChange(mq.matches));
""");

@ClientCallable
public void onDarkModeChange(boolean dark) {
    darkModeSignal.set(dark);
}

Once bridged, the events participate in all normal signal patterns (computed, effects, bindings).

Always clean up browser-side listeners on detach:

addDetachListener(detach -> {
    detach.getUI().getPage().executeJs("if (window[$0]) { window[$0](); delete window[$0]; }", key);
});

12. Computed Signal Chains

Complex derivation should be broken into named steps, not one large lambda:

// UC14: Derived UI state from a single enum signal — each step is clear
Signal<Boolean> isLoadingSignal = stateSignal.map(state ->
    state == LoadingState.LOADING || state == LoadingState.GENERATING);

// Each metric card gets its own computed signal
Signal<String> revenueSignal = reportDataSignal.map(report ->
    report.isEmpty() ? "" : String.format("$%,d", report.getTotalRevenue()));

Signal<String> ordersSignal = reportDataSignal.map(report ->
    report.isEmpty() ? "" : String.format("%,d", report.getTotalOrders()));

Benefit: Each signal has a clear name and single purpose. When debugging reactivity, you trace the chain by name rather than reading a sprawling lambda.

UC10: Composing unrelated signals into one computed summary:

Signal<String> summarySignal = Signal.computed(() -> {
    String uploadPart = switch (uploadStateSignal.get()) { ... };
    String shortcutPart = "Shortcut: " + lastShortcutSignal.get();
    String darkPart = darkModeSignal.get() ? "Theme: dark" : "Theme: light";
    return uploadPart + "  |  " + shortcutPart + "  |  " + darkPart;
});

This is idiomatic: bridged event signals from multiple unrelated sources composed into one reactive display signal.


13. Session-Scoped Signals

For user preferences that must survive navigation between views, inject the signal carrier as a @SessionScope Spring bean:

@Component
@SessionScope
public class UserPreferences {
    public static final String DEFAULT_COLOR = "var(--lumo-base-color)";
    private final ValueSignal<String> backgroundColorSignal = new ValueSignal<>(DEFAULT_COLOR);

    public ValueSignal<String> backgroundColorSignal() {
        return backgroundColorSignal;
    }
}

// In a view or layout:
public class MainLayout extends AppLayout {
    public MainLayout(UserPreferences preferences) {
        Signal<String> colorSignal = preferences.backgroundColorSignal();
        getContent().getStyle().bind("background-color", colorSignal);
    }
}

// In a settings view:
public class UseCase20View extends VerticalLayout {
    public UseCase20View(UserPreferences preferences) {
        ValueSignal<String> colorSignal = preferences.backgroundColorSignal();
        colorPicker.bindValue(colorSignal, colorSignal::set);
    }
}

Key: The signal lives in the bean (not the view). Views receive and bind to the same signal instance. When any view updates it, all bound views update automatically — including across navigation.


14. Async Operations and Signals

Rule: Signals are thread-safe. Set signal values directly from CompletableFuture callbacks — no UI.access() needed.

private final ValueSignal<LoadingState> stateSignal = new ValueSignal<>(LoadingState.IDLE);
private final ValueSignal<ReportData> reportDataSignal = new ValueSignal<>(ReportData.empty());

private void loadReport() {
    stateSignal.set(LoadingState.LOADING);

    // Read non-reactive state before async — peek() because we are not in reactive context
    boolean shouldFail = shouldFailSignal.peek();

    analyticsService.fetchReportData(shouldFail)
        .thenCompose(rawData -> {
            stateSignal.set(LoadingState.GENERATING);    // direct, no UI.access()
            return analyticsService.generateReportFromData(rawData, shouldFail);
        })
        .thenAccept(report -> {
            reportDataSignal.set(report);                // direct, no UI.access()
            stateSignal.set(LoadingState.SUCCESS);
        })
        .exceptionally(error -> {
            stateSignal.set(LoadingState.ERROR);
            return null;
        });
}

UI visibility switches reactively from the signal, not from the async callbacks:

idleContent.bindVisible(() -> stateSignal.get() == LoadingState.IDLE);
loadingContent.bindVisible(isLoadingSignal);
successContent.bindVisible(() -> stateSignal.get() == LoadingState.SUCCESS);
errorContent.bindVisible(() -> stateSignal.get() == LoadingState.ERROR);

Per-item async loading (UC19):

Use a ListSignal<DataItem> where each item has its own ValueSignal<LoadingState>. Individual items update independently without causing the whole list to re-render.


15. Debounce with Manual Scheduling

Signals have no built-in debounce. Use a ScheduledExecutorService manually:

private final ValueSignal<String> instantQuerySignal = new ValueSignal<>("");
private final ValueSignal<String> debouncedQuerySignal = new ValueSignal<>("");
private final ScheduledExecutorService debounceExecutor = Executors.newSingleThreadScheduledExecutor();
private volatile ScheduledFuture<?> pendingDebounce = null;

// EAGER mode sends on every keystroke
searchField.setValueChangeMode(ValueChangeMode.EAGER);
searchField.bindValue(instantQuerySignal, value -> {
    instantQuerySignal.set(value);
    scheduleDebouncedSearch(value);
});

private void scheduleDebouncedSearch(String query) {
    ScheduledFuture<?> pending = pendingDebounce;
    if (pending != null) pending.cancel(false);

    pendingDebounce = debounceExecutor.schedule(() -> {
        debouncedQuerySignal.set(query);     // thread-safe
        performSearch(query);
    }, 1000, TimeUnit.MILLISECONDS);
}

@Override
protected void onDetach(DetachEvent e) {
    super.onDetach(e);
    if (pendingDebounce != null) pendingDebounce.cancel(false);
    debounceExecutor.shutdownNow();
}

instantQuerySignal and debouncedQuerySignal serve different UI roles: instant shows "what you typed", debounced shows "what was searched". Displaying both is a good UX pattern for revealing debounce behavior to users.


16. Binder + Signal Integration

Vaadin Binder exposes two signal integration points:

validationStatusSignal()

Binder<AccountData> binder = new Binder<>(AccountData.class);
// ... add field bindings with validators ...

// Use Binder's validation signal to drive submit button state
submitButton.bindEnabled(() ->
    binder.validationStatusSignal().get().isOk()
    && submissionStateSignal.get() != SubmissionState.SUBMITTING);

validationStatusSignal() returns a reactive Signal<BinderValidationStatus<T>> that updates on every validation event. .isOk() is true when all bound fields pass validation.

Binding.valueSignal() — cross-field validation

When a field validator needs the value of another field (e.g., confirm password), read the other binding's valueSignal() inside the validator:

var passwordBinding = binder.forField(passwordField)
    .withValidator(value -> value != null && value.length() >= 8, "Password too short")
    .bind(AccountData::getPassword, AccountData::setPassword);

binder.forField(confirmField)
    .withValidator(
        value -> value != null && value.equals(passwordBinding.valueSignal().get()),
        "Passwords do not match")
    .bind(AccountData::getConfirmPassword, AccountData::setConfirmPassword);

passwordBinding.valueSignal().get() reads the current field signal value at validation time — reactive cross-field validation without manual listeners.

Binder + ValueSignal for submission state

A common pattern in UC01:

ValueSignal<SubmissionState> submissionStateSignal = new ValueSignal<>(SubmissionState.IDLE);

// Button text reacts to submission state
submitButton.bindText(submissionStateSignal.map(state -> switch (state) {
    case IDLE -> "Create Account";
    case SUBMITTING -> "Creating...";
    case SUCCESS -> "Success!";
    case ERROR -> "Retry";
}));

// Theme variant reacts to success state
submitButton.bindThemeVariant(ButtonVariant.LUMO_SUCCESS,
    submissionStateSignal.map(SubmissionState.SUCCESS::equals));
submitButton.bindThemeVariant(ButtonVariant.LUMO_PRIMARY,
    Signal.not(submissionStateSignal.map(SubmissionState.SUCCESS::equals)));

17. Element.bindAttribute()

For non-component DOM elements (e.g., raw SVG elements), use Element.bindAttribute():

Element rect = new Element("rect");

// Single-signal binding
rect.bindAttribute("fill", rectFillSignal);
rect.bindAttribute("opacity", rectOpacitySignal.map(String::valueOf));

// Multi-signal lambda
rect.bindAttribute("stroke-width", () -> {
    int base = rectStrokeWidthSignal.get();
    boolean selected = selectedShapeSignal.get() == 0;
    return String.valueOf(selected ? base + 2 : base);
});

// Signal.computed() for expensive transform calculations
rect.bindAttribute("transform", Signal.computed(() -> {
    int centerX = rectXSignal.get() + rectWidthSignal.get() / 2;
    int centerY = rectYSignal.get() + rectHeightSignal.get() / 2;
    return String.format("rotate(%d %d %d)", rectRotationSignal.get(), centerX, centerY);
}));

Same rules apply: single signal → direct; multi-signal → lambda or Signal.computed().

Similarly for inline styles at the element level:

dot.getStyle().bind("background-color",
    () -> activeSignal.get() ? "var(--lumo-contrast-80pct)" : "var(--lumo-contrast-30pct)");

18. Signal.not() and Boolean Derivations

Signal.not() creates a computed signal that negates a boolean signal. Preferred over .map(v -> !v) for clarity:

ValueSignal<Boolean> loading = new ValueSignal<>(true);
Signal<Boolean> notLoading = Signal.not(loading);

button.bindEnabled(notLoading);

For conditional theme variants, the complement pattern from UC01:

Signal<Boolean> isSuccess = stateSignal.map(SubmissionState.SUCCESS::equals);
button.bindThemeVariant(ButtonVariant.LUMO_SUCCESS, isSuccess);
button.bindThemeVariant(ButtonVariant.LUMO_PRIMARY, Signal.not(isSuccess));

19. Custom Equality Checkers

By default, signal equality uses Objects.equals(). Override to suppress updates when values are semantically equal:

// Skip updates for case-insensitive string equality
ValueSignal<String> name = new ValueSignal<>("John",
    (a, b) -> a != null && a.equalsIgnoreCase(b));

name.set("john");  // No update triggered
name.set("Jane");  // Update triggered

For ListSignal, the equality checker applies to all entry ValueSignal instances in the list:

ListSignal<String> items = new ListSignal<>(
    (a, b) -> a != null && a.equalsIgnoreCase(b));

Use this when your value type has meaningful equality that differs from Objects.equals(), to avoid unnecessary re-renders.


20. Memory and Lifecycle

Signals as class fields (not constructor-local variables)

// CORRECT: signals as fields — reachable, lifecycle managed by component
public class MyView extends VerticalLayout {
    private final ValueSignal<String> stateSignal = new ValueSignal<>("idle");
}

// WRONG: signal in constructor scope — effect keeps a closure reference,
// but the signal itself may be GC'd if nothing else holds a reference
public MyView() {
    ValueSignal<String> localSignal = new ValueSignal<>("idle");
    Signal.effect(this, () -> label.setText(localSignal.get()));
    // localSignal not stored anywhere — risky
}

ListSignal entries are themselves signals

ListSignal<String> items = new ListSignal<>();
ValueSignal<String> entry = items.insertLast("value");

// entry is a live ValueSignal — keep a reference if you need to update it later
entry.set("updated");   // updates the item in the list reactively

// Or retrieve it later via peek()
items.peek().get(0).set("updated");

Signal.runWithoutTransaction() — last resort

Effects run inside a read-only transaction. Modifying a signal from inside an effect normally throws. If you absolutely must:

Signal.effect(component, () -> {
    String value = sourceSignal.get();
    // WARNING: infinite loop risk — only use when you are certain
    Signal.runWithoutTransaction(() -> otherSignal.set(value));
});

Prefer Signal.computed() instead for derived values that depend on another signal.

Signal.untracked() — read without subscribing

For reading signal values inside an effect for logging or analytics without creating a dependency:

Signal.effect(component, () -> {
    String primary = primarySignal.get();   // tracked dependency
    Signal.untracked(() -> {
        String analytics = analyticsSignal.get();   // NOT a dependency
        log(analytics);
    });
});

21. Anti-Patterns Index

Anti-Pattern Correct Pattern
get() in click listener / onAttach() / service callback Use peek() outside reactive contexts
signal.get() inside an effect when you also write to that signal Use signal.peek() for the read
One large Signal.effect() doing many unrelated things Split into separate bindXxx() calls or separate effects
Signal.effect() for simple property binding Use bindText(), bindVisible(), bindEnabled(), etc.
Storing state in regular fields alongside signals All mutable UI state in signals
Signal.unboundEffect() without storing the Registration Always store and call remove()
cartItemsSignal.get().stream().filter(...)... inside an effect (subscribes to every item) Use peek() in scan-and-write operations
new ValueSignal<>() as a local constructor variable when used in effects Store as a class field
Calling UI.access() when updating a local signal from a background thread Not needed — local signals are thread-safe
Using Signal.computed() for derived values but mutating the source inside an effect Source mutations go through signal.set() / signal.update() outside the computed
bindValue(signal) only (read-only) when you need two-way binding bindValue(readSignal, writeFn) for two-way
Manually scanning ListSignal.get() to update items in an effect Use bindChildren() for component lists
Not cleaning up ScheduledExecutorService on onDetach() Always shutdownNow() + cancel pending futures on detach
Modifying a mutable object retrieved from a signal directly Use modify(Consumer) or update(UnaryOperator)

Source: vaadin/signals-cases all 20+ use cases + Vaadin 25.2 official documentation (effects-computed.md, local-signals.md, building-ui.md, shared-signals.md, usage-examples/).*

Clone this wiki locally