Docs

Effects and Computed Signals

Using effects and computed signals for reactive UI updates.

Effects and computed signals are the core mechanisms for custom reactive logic. Effects automatically re-run when their signal dependencies change, while computed signals derive values from other signals.

For binding signals directly to component properties like text, visibility, and form field values, see Component Bindings. This section covers effects for custom logic and computed signals for derived values.

When to Use Effects

Use direct binding methods (covered in Component Bindings) when:

  • Binding text content with bindText()

  • Binding visibility with bindVisible()

  • Binding enabled state with bindEnabled()

  • Binding form field values with bindValue()

  • Binding styles, themes, or class lists

Use Signal.effect() when:

  • You need custom side effects beyond property binding

  • You’re working with component APIs that don’t have binding methods

  • You need to execute logic that isn’t just setting a property

Component Effects

Effects are callbacks that run immediately when created and automatically re-run when any signal value they depend on changes. Dependencies are automatically managed based on the signals that were used the last time the callback was run.

Creating Effects

The Signal.effect() method creates an effect bound to a component’s lifecycle. The effect is active while the component is attached and inactive while detached.

Source code
Java
Chart chart = new Chart(ChartType.AREASPLINE);
ListSeries series = new ListSeries("My Series", new Number[0]);
ListSignal<Number> seriesSignal = new ListSignal<>();
// initialization details omitted

Signal.effect(chart, () -> {
    // This code runs immediately and again whenever seriesSignal changes
    series.setData(seriesSignal.get().stream()
        .map(Signal::get).toArray(Number[]::new));
});

The method returns a Registration instance that can be used to remove the effect when it’s no longer needed:

Source code
Java
Registration effectRegistration = Signal.effect(chart, () -> {
    // effect logic
});

// Later, when you need to stop the effect
effectRegistration.remove();

Contextual Effects

Use the contextual variant of Signal.effect() to receive an EffectContext parameter that provides information about why the effect is running:

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

    if (!ctx.isInitialRun() && ctx.isBackgroundChange()) {
        span.getElement().flashClass("highlight");
    }
});

The EffectContext provides:

  • isInitialRun() — true on the first execution of the effect

  • isBackgroundChange() — true when the effect was triggered by a change from another user session or a background thread

Important

Always read the signal values you depend on before branching on the context flags, as shown above where priceSignal.get() is read unconditionally. Reading a signal only inside a conditional branch — such as if (!ctx.isInitialRun()) — breaks dependency tracking. See Read Signals Unconditionally for details.

When Effects Run

A signal change made while the component’s UI is the current UI — in an event listener of that UI, or inside UI.access() for that UI — runs the dependent effects and bindings of attached components before set() or update() returns:

Source code
Java
ValueSignal<Integer> count = new ValueSignal<>(0);
Span label = new Span();
label.bindText(count.map(c -> "Count: " + c));
add(label);

count.set(5);
label.getText(); // Returns "Count: 5"

When one action changes several signals in a row, each change has updated the UI before the next one starts. Code that depends on the order can rely on this, for example code that saves the value of a filter, replaces the choices of the filter field, and then sets the filter back. Shared signals changed inside Signal.runInTransaction() are an exception: the changes are applied when the transaction commits, so the effects run then. See Transactions.

A change made on any other thread, such as a background thread or a request of another UI or session, doesn’t run the effect inline. The effect is queued and runs later through UI.access(), and EffectContext.isBackgroundChange() returns true for that run. Apart from the first run when they’re created, effects and bindings of detached components don’t run. When the component is attached again, the effect runs if a dependency has changed in the meantime.

Changes from background threads reach the browser only when server push is enabled. See Local Signals for updating signals from background threads.

Building UI Components in an Effect

An effect can create UI components, for example to rebuild a detail form whenever the selected item changes. Every get() call made while the effect runs becomes a dependency of the effect, including calls in the constructors and methods it invokes. Bindings and effects created inside the effect, however, track their own signals: a bindValue(), bindText(), or Signal.effect() call inside the effect doesn’t make the outer effect depend on the bound signal.

Source code
Java
Signal.effect(detailLayout, () -> {
    Person person = selectedPerson.get(); // Dependency of the effect
    detailLayout.removeAll();
    if (person != null) {
        TextField name = new TextField("Name");
        // Not a dependency: the binding tracks nameSignal itself
        name.bindValue(nameSignal, nameSignal::set);
        detailLayout.add(name);
    }
});

Bindings and effects are inactive while their component is detached, so the components that the effect removes stop updating. Remove the old components when the effect re-runs, as removeAll() does above. Use Signal.untracked() around the construction only when it reads signals with get() that the effect shouldn’t re-run for. See Executing Code without Tracking.

Custom Effects for Non-Bindable Properties

Use effects for component properties that don’t have direct binding methods:

Source code
Java
Signal.effect(component, () -> {
    component.setCustomProperty(signal.get());
});

Computed Signals

Computed signals derive their values from other signals. They automatically update when any of their dependencies change.

Source code
Java
Signal<String> fullName = Signal.computed(() -> {
    return firstNameSignal.get() + " " + lastNameSignal.get();
});

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 Signal.cached() to avoid repeating the work on every read (see Caching with Signal.cached()).

Combining Multiple Signals

The primary use case for computed signals is combining values from multiple source signals:

Source code
Java
ValueSignal<Double> price = new ValueSignal<>(100.0);
ValueSignal<Double> quantity = new ValueSignal<>(2.0);
ValueSignal<Double> taxRate = new ValueSignal<>(0.1);

Signal<Double> total = Signal.computed(() -> {
    double subtotal = price.get() * quantity.get();
    return subtotal * (1 + taxRate.get());
});
// total.peek() returns 220.0 (subject to floating-point rounding)

quantity.set(3.0);
// total.peek() now returns 330.0

Caching with Signal.cached()

Neither a Signal.computed() signal nor an inline lambda passed to a binding caches its value: the computation callback runs every time the value is read. This keeps the computation lightweight, but reading the value of an expensive computation from several places repeats the work.

Use Signal.cached() to add caching to such a computation. 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 code
Java
Signal<Integer> expensiveComputation = Signal.cached(() ->
    performExpensiveCalculation(inputSignal.get()));

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

inputSignal.set(newInput);
expensiveComputation.peek(); // Recomputes because a dependency changed

Use plain 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 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 Signal.cached() useful for cutting off update chains when an expensive intermediate result is unchanged.

Signal Mapping

The map() method transforms a signal’s value. Use this when deriving a value from exactly one signal:

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

Signal<String> ageCategory = age.map(a ->
    a < 18 ? "Child" : (a < 65 ? "Adult" : "Senior"));

Signal<Boolean> adult = age.map(a -> a >= 18);

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

Tip

The read-only map() method shown above creates a one-way transformation. For two-way binding scenarios where changes need to flow in both directions (for example, binding a checkbox to a property of a record), use updater() or modifier(). See Two-Way Signal Mapping for details.

Negating Boolean Signals

The Signal.not() method creates a computed signal that negates a boolean signal:

Source code
Java
ValueSignal<Boolean> loading = new ValueSignal<>(true);

// Create a negated signal
Signal<Boolean> notLoading = Signal.not(loading);

Reading without Dependencies

Using peek() to Avoid Dependencies

Use peek() to read a signal’s value without registering a dependency:

Source code
Java
Signal.effect(component, () -> {
    // get() registers a dependency - effect will re-run when nameSignal changes
    String name = nameSignal.get();

    // peek() doesn't register a dependency - effect won't re-run when countSignal changes
    int count = countSignal.peek();
});

Executing Code without Tracking

The Signal.untracked() method executes a block of code without tracking any signal dependencies and returns the value of the block:

Source code
Java
Signal.effect(component, () -> {
    // This read is tracked
    String trackedValue = trackedSignal.get();

    // Everything inside untracked() is not tracked
    String untrackedValue = Signal.untracked(() -> anotherSignal.get());
    processValue(untrackedValue);
});

This is useful when you need to read signal values for logging, analytics, or other side effects without creating dependencies.

Standalone Effects

A standalone signal effect can be used for effects that aren’t related to any UI component. The effect remains active until explicitly cleaned up.

Source code
Java
Registration cleanup = Signal.unboundEffect(() -> {
    System.out.println("Counter updated to " + counter.get());
});

// Later, when the effect is no longer needed
cleanup.remove();

A standalone effect runs without holding the session lock, even when it’s created while holding the lock. If the effect callback modifies components or other UI state, it must acquire the lock with UI.access().

Warning
Standalone effects can lead to memory leaks through any instances referenced by the closure of the effect callback. Always store the Registration and call remove() when the effect is no longer needed.

Dependency Tracking

Dependencies are tracked dynamically based on which signals are actually read during execution:

Source code
Java
ValueSignal<Boolean> showDetails = new ValueSignal<>(false);
ValueSignal<String> summary = new ValueSignal<>("");
ValueSignal<String> details = new ValueSignal<>("");

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

When showDetails is false, changes to details won’t trigger the effect because it wasn’t read in the last execution.

Read Signals Unconditionally

Because dependencies are tracked only for the signals actually read during a run, where you read a signal matters. Two related rules follow from this:

  • An effect (and a computed signal) must read at least one signal on every run. A run that reads no signal throws a MissingSignalUsageException with the message "Effect action must read at least one signal value." Such a callback could never be triggered again, so it’s reported as a programming error rather than silently ignored.

  • A signal read only inside a conditional branch becomes a dependency only on the runs where that branch executes. If the branch doesn’t run, the signal isn’t tracked, and later changes to it won’t re-run the effect.

The safest pattern is the same as React hooks: read all the signal values you depend on at the top of the callback, then apply conditional logic afterwards.

Guarding Reads Behind Context Flags

A common way to hit both rules at once is to gate a signal read behind an EffectContext flag:

Source code
Java
// Broken: throws MissingSignalUsageException on the initial run
Signal.effect(this, ctx -> {
    if (!ctx.isInitialRun()) {
        // Skipped on the first run, so no signal is read and the
        // effect has no dependencies to ever trigger a later run
        Notification.show("Value changed: " + valueSignal.get());
    }
});

On the initial run, isInitialRun() is true, the branch is skipped, no signal is read, and the effect throws. Even if it didn’t throw, the effect would never have a dependency to re-run on.

Read the signal unconditionally, then branch on the context flag:

Source code
Java
// Correct: the signal is always read, so the dependency is always tracked
Signal.effect(this, ctx -> {
    String value = valueSignal.get();
    if (!ctx.isInitialRun()) {
        Notification.show("Value changed: " + value);
    }
});

Branching Between Different Signals

The same pitfall appears when each branch reads a different signal:

Source code
Java
// Broken: signal2 is never tracked, so the effect never reacts to it
Signal.effect(this, ctx -> {
    if (ctx.isInitialRun()) {
        component.setText(signal1.get()); // tracked on the first run only
    } else {
        component.setText(signal2.get()); // never reached, never tracked
    }
});

Only signal1 is read on the initial run, so only signal1 becomes a dependency. Because the effect never re-runs on a signal2 change, the else branch is never reached. Read both signals up front so both are tracked:

Source code
Java
// Correct: both signals are read, so both are dependencies
Signal.effect(this, ctx -> {
    String first = signal1.get();
    String second = signal2.get();
    component.setText(ctx.isInitialRun() ? first : second);
});
Note

Conditional reads are still valid when every branch reads a signal that itself controls the condition — as in the Dependency Tracking example, where showDetails.get() is always read and decides which other signal to read. The pitfall is specifically reading a signal only on some runs, especially behind a context flag that is constant for a given run.

Bindings and Computed Signals

The same rules apply to the callback of a computed signal and to a lambda passed to a binding method, because a binding runs as an effect. A computed signal doesn’t count as a read by itself: only the signals that its callback reads do. The short-circuit operators && and || are an easy way to skip every read:

Source code
Java
// Broken: when canWrite is false, no signal is read, and
// bindVisible() throws MissingSignalUsageException
save.bindVisible(Signal.computed(() -> canWrite && shown.get() != null));

// Correct: the signal is read first, on every run
save.bindVisible(Signal.computed(() -> shown.get() != null && canWrite));

The binding runs its callback once when it’s created, even if the component isn’t attached yet, so the exception comes from the binding method call. After that first run, the binding is inactive until the component is attached. The exception message mentions an effect action even though the code only creates a binding.

Best Practices

Prefer Direct Bindings Over Effects

Use component binding methods for property updates. Effects are for custom logic that bindings can’t handle:

Source code
Java
// Preferred: Direct binding
label.bindText(signal);

// Avoid: Effect for simple property binding
Signal.effect(label, () -> label.setText(signal.get()));

See Component Bindings for available binding methods.

Avoid Changing Signals in Effects

Updating a signal in reaction to another signal might cause an infinite loop. If an effect changes a signal that it depends on, directly or through other effects, Flow detects the loop, deactivates the effect, and reports an IllegalStateException to the error handler.

Use a computed signal when one signal’s value depends on another:

Source code
Java
// Good: Use computed signal for derived values
Signal<String> derivedValue = Signal.computed(() -> {
    return sourceSignal.get().toUpperCase();
});

Modify a signal from an effect only if absolutely necessary, and only when you’re certain there’s no risk of infinite loops:

Source code
Java
Signal.effect(component, () -> {
    String value = oneSignal.get();

    // WARNING: This might lead to infinite loops.
    // Do this only if absolutely necessary.
    otherSignal.set(value);
});

Keep Effects Focused

Each effect should have a single responsibility. For multiple property updates, use separate bindings.

Using separate effects or bindings for different concerns improves efficiency: when a signal changes, only the components that depend on it are updated. For example, in a dashboard with charts and cards, separate effects ensure that a single data change triggers updates only in the specific dependent component, rather than re-rendering unrelated parts of the UI.

Source code
Java
// Good: Separate bindings for separate concerns
nameLabel.bindText(userSignal.map(User::name));
ageLabel.bindText(userSignal.map(u -> String.valueOf(u.age())));

// Avoid: One large effect doing multiple things
Signal.effect(container, () -> {
    nameLabel.setText(userSignal.get().name());
    ageLabel.setText(String.valueOf(userSignal.get().age()));
    // ... more updates
});

Updated –