> Markdown version of [Real-Time Dashboard](https://vaadin.com/docs/next/flow/ui-state/usage-examples/realtime-dashboard). Section index: [llms.txt](https://vaadin.com/docs/next/flow/llms.txt)

# Real-Time Dashboard with Signals

This guide demonstrates how to build a real-time dashboard using signals for reactive state management. The example shows a clean separation between backend data services and UI state, where signals serve as the reactive layer connecting them.

## <a id="architecture-overview"></a>Architecture Overview

The dashboard follows a signal-based architecture with clear separation of concerns:

**Backend service layer** - Generates data and doesn’t work with signals directly. It only calls callbacks with plain data objects (records, POJOs).

**View callback layer** - Receives data from the service and updates signals. No UI manipulation happens here, only signal updates.

**UI binding layer** - Components and charts are bound to signals via effects. When signals change, effects automatically update the UI.

**No manual state management** - No state stored in regular class fields. All state lives in signals. No manual listeners, no `UI.access()` calls, no manual chart redraws.

This separation makes the code easier to test, maintain, and reason about. The service layer can be tested without UI concerns, and the UI layer automatically stays in sync with state.

## <a id="design-principles"></a>Design Principles

The dashboard demonstrates key signal design principles:

**Signals as the single source of truth** - All dashboard state (metrics, timeline data, service health) is stored in signals. Regular instance fields are only used for constants or non-reactive data.

**Service-to-signal pattern** - Backend services call callbacks with plain data objects. The callback only updates signals, never touches the UI directly.

**Effect-based UI updates** - Charts and components update via effects that watch signals. No manual update calls needed.

**Separation of concerns** - Data generation (service), state management (signals), and UI rendering (effects) are cleanly separated.

**No listeners** - Components don’t need change listeners. Effects automatically detect signal changes and update the UI.

## <a id="state-declaration"></a>State Declaration

All dashboard state is declared as signals:

`RealtimeDashboard.java`

```java
private static final int TIMELINE_POINTS = 12;

// All state is stored in signals - no regular instance fields for state
private final ValueSignal<Number> currentUsersSignal = new ValueSignal<>(0);
private final ValueSignal<Number> viewEventsSignal = new ValueSignal<>(0);

private final ListSignal<String> timelineCategoriesSignal = new ListSignal<>();
private final ListSignal<Number> berlinTimelineSignal = new ListSignal<>();
private final ListSignal<Number> londonTimelineSignal = new ListSignal<>();
private final ListSignal<Number> newYorkTimelineSignal = new ListSignal<>();
```

Notice there are no regular instance fields for storing metric values or chart data. Everything that changes over time is a signal. This makes the reactive state explicit and easy to identify.

The signals are organized by type:

- `ValueSignal<Number>` for simple metrics (current users, view events)

- `ListSignal<Number>` for time-series data (Berlin, London, New York timelines)

- `ListSignal<String>` for category labels (timestamps)

Each signal can be independently updated and watched, creating a flexible reactive system.

## <a id="service-integration"></a>Service Integration

The view registers a callback with the scheduler service:

`RealtimeDashboard.java`

```java
public RealtimeDashboard(SchedulerService schedulerService) {
    // Create UI components bound to signals
    add(createHighlightCard("Current users", currentUsersSignal,
            this::formatNumber),
            createHighlightCard("View events", viewEventsSignal,
                    this::formatCompactNumber),
            createViewEventsChart());

    // Register with scheduler - service will call our callback
    // No need for UI.access() - signals handle thread safety
    schedulerService.scheduleDashboardDataUpdate(this::onDataUpdate);
}
```

The service doesn’t know about signals or UI components. It just calls the callback periodically with new data. The comment highlights an important point: you don’t need `UI.access()` because signals handle thread safety internally.

> **Note:** For real-time updates to reach users immediately, you need to [enable push](https://vaadin.com/docs/next/flow/advanced/server-push.md#push.configuration.enabling) in your application. Without push, updates only appear when the user interacts with the UI.

## <a id="data-update-callback"></a>Data Update Callback

The callback receives plain data objects and updates signals:

`RealtimeDashboard.java`

```java
/**
 * Callback invoked by the scheduler service with new dashboard data. This
 * method ONLY updates signals - no UI manipulation, no listeners. UI
 * updates happen automatically via effects.
 */
private void onDataUpdate(DashboardData data) {
    // Update simple value signals
    currentUsersSignal.set(data.currentUsers());
    viewEventsSignal.set(data.viewEvents());

    // Update timeline - maintain sliding window of last N points
    updateTimelineSignal(berlinTimelineSignal, data.berlinValue());
    updateTimelineSignal(londonTimelineSignal, data.londonValue());
    updateTimelineSignal(newYorkTimelineSignal, data.newYorkValue());

    // Update categories
    if (timelineCategoriesSignal.peek().size() >= TIMELINE_POINTS) {
        timelineCategoriesSignal
                .remove(timelineCategoriesSignal.peek().getFirst());
    }
    timelineCategoriesSignal.insertLast(data.timestamp());
}
```

This method demonstrates the service-to-signal pattern:

1. Receive plain data object (`DashboardData`)

2. Extract values and update signals

3. No UI manipulation, no chart redraws, no manual updates

The callback is pure signal updates. It doesn’t care what UI components are listening or how they render the data. This separation makes the code easier to test and maintain.

### <a id="managing-sliding-window-data"></a>Managing Sliding Window Data

The helper method maintains a sliding window of the last N data points:

`RealtimeDashboard.java`

```java
private void updateTimelineSignal(ListSignal<Number> signal,
        Number newValue) {
    if (signal.peek().size() >= TIMELINE_POINTS) {
        signal.remove(signal.peek().getFirst());
    }
    signal.insertLast(newValue);
}
```

This pattern is common for time-series dashboards: when new data arrives, remove the oldest point and add the new one. The `ListSignal` API makes this straightforward with `remove()` and `insertLast()`.

## <a id="chart-updates-with-effects"></a>Chart Updates with Effects

Charts update automatically via effects that watch signals:

`RealtimeDashboard.java`

```java
/**
 * Creates a chart that updates automatically when signals change. No manual
 * chart.drawChart() calls needed - effects handle it.
 */
private Component createViewEventsChart() {
    Chart chart = new Chart(ChartType.AREASPLINE);
    Configuration conf = chart.getConfiguration();

    XAxis xAxis = new XAxis();
    conf.addxAxis(xAxis);

    // Create chart series
    ListSeries berlinSeries = new ListSeries("Berlin", new Number[0]);
    ListSeries londonSeries = new ListSeries("London", new Number[0]);
    ListSeries newYorkSeries = new ListSeries("New York", new Number[0]);

    // Each series gets its own effect to update data
    bindChartData(chart, berlinSeries, berlinTimelineSignal);
    bindChartData(chart, londonSeries, londonTimelineSignal);
    bindChartData(chart, newYorkSeries, newYorkTimelineSignal);

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

    conf.addSeries(berlinSeries);
    conf.addSeries(londonSeries);
    conf.addSeries(newYorkSeries);

    // Separate effect to trigger chart redraw when any data changes
    Signal.effect(chart, () -> {
        berlinTimelineSignal.get();
        londonTimelineSignal.get();
        newYorkTimelineSignal.get();
        chart.drawChart();
    });

    return chart;
}
```

This code demonstrates several important patterns:

### <a id="per-series-effects"></a>Per-Series Effects

Each chart series has its own effect via `bindChartData()`:

```java
// Each series gets its own effect to update data
bindChartData(chart, berlinSeries, berlinTimelineSignal);
bindChartData(chart, londonSeries, londonTimelineSignal);
bindChartData(chart, newYorkSeries, newYorkTimelineSignal);

// The binding method creates an effect per series
private static void bindChartData(Chart chart, ListSeries series,
        ListSignal<Number> signal) {
    Signal.effect(chart, () -> {
        series.setData(signal.get().stream()
            .map(Signal::get)
            .toArray(Number[]::new));
    });
}
```

Each effect watches one `ListSignal` and updates one series. This granular approach means only the affected series updates when its signal changes, not the entire chart.

The effect reads the list of signals (`signal.get()`), then maps each entry signal to its value with `.map(Signal::get)`. This is necessary because `ListSignal` entries are themselves signals (`ValueSignal<Number>`).

### <a id="multiple-effects-for-different-concerns"></a>Multiple Effects for Different Concerns

The chart has separate effects for different update concerns:

1. **Data binding effects** - One per series, updates series data when its signal changes

2. **Category effect** - Updates x-axis labels when `timelineCategoriesSignal` changes

3. **Redraw effect** - Watches all timeline signals and triggers chart redraw

This separation of concerns makes the code easier to understand and debug. Each effect has a single responsibility.

### <a id="coordinated-redraw-effect"></a>Coordinated Redraw Effect

The redraw effect demonstrates how to coordinate multiple signals:

```java
Signal.effect(chart, () -> {
    berlinTimelineSignal.get();
    londonTimelineSignal.get();
    newYorkTimelineSignal.get();
    chart.drawChart();
});
```

This effect calls `get()` on each timeline signal to register dependencies, then calls `chart.drawChart()`. When any of the three signals change, the effect re-runs and redraws the chart.

> **Tip:** Reading a signal with `get()` inside an effect creates a dependency. The effect automatically re-runs when that signal changes. You can read multiple signals to create an effect that watches several sources.

## <a id="change-tracking-pattern"></a>Change Tracking Pattern

The highlight card demonstrates tracking value changes over time using the Change record pattern:

`RealtimeDashboard.java`

```java
/**
 * A highlight card showing a metric value with percentage change indicator.
 * Demonstrates: computed signals, signal mapping, and derived state.
 */
private static final class HighlightCard extends VerticalLayout {
    // Internal record tracking previous and current values
    record Change(double previous, double current) {
    }

    private HighlightCard(String title, ValueSignal<Number> signal,
            Function<Number, String> format) {

        // Create a signal to track previous-current value pairs
        ValueSignal<Change> changeSignal = new ValueSignal<>(new Change(
                signal.peek().doubleValue(), signal.peek().doubleValue()));

        // Effect: Update changeSignal when the main signal changes
        // This tracks the previous value to calculate percentage change
        Signal.unboundEffect(() -> {
            double current = signal.get().doubleValue();
            double previous = changeSignal.peek().current();
            changeSignal.set(new Change(previous, current));
        });

        // Computed signal: Calculate percentage change from Change record
        Signal<Double> percentageSignal = changeSignal
                .map(change -> calculatePercentageChange(change.current(),
                        change.previous()));

        // Derived signals: Map percentage to display properties
        Signal<String> prefixSignal = percentageSignal.map(this::getPrefix);
        Signal<VaadinIcon> iconSignal = percentageSignal.map(this::getIcon);
        Signal<Boolean> successSignal = percentageSignal
                .map(percentage -> percentage > 0);

        // Build UI components
        H2 h2 = new H2(title);
        h2.addClassNames(FontWeight.NORMAL, Margin.NONE,
                TextColor.SECONDARY, FontSize.XSMALL);

        // Bind value display to formatted signal
        Span valueSpan = new Span();
        valueSpan.addClassNames(FontWeight.SEMIBOLD, FontSize.XXXLARGE);
        valueSpan.bindText(signal.map(format::apply));

        // Bind icon to computed signal
        Icon icon = new Icon(iconSignal);

        // Create badge with conditional theme binding
        Badge badge = new Badge(icon);
        badge.getElement().getThemeList().bind("success", successSignal);
        badge.getElement().getThemeList().bind("error",
                Signal.not(successSignal));

        // Bind percentage text with prefix
        badge.bindText(prefixSignal
                .map(prefix -> prefix + percentageSignal.get()));

        add(h2, valueSpan, badge);
        getStyle().setGap("5px");
    }

    private String getPrefix(double percentage) {
        if (percentage == 0) {
            return "±";
        } else if (percentage > 0) {
            return "+";
        } else {
            return "";
        }
    }

    private VaadinIcon getIcon(double percentage) {
        return percentage < 0 ? VaadinIcon.ARROW_DOWN : VaadinIcon.ARROW_UP;
    }

    private double calculatePercentageChange(double current,
            double previous) {
        if (previous == 0.0) {
            return 0.0;
        }
        double percent = ((current - previous) / Math.abs(previous))
                * 100.0;
        return Math.round(percent * 10.0) / 10.0;
    }
}
```

This pattern uses:

1. **Change record** - Holds both `previous` and `current` values

2. **Change signal** - A `ValueSignal<Change>` tracking the value pair

3. **Synchronization effect** - Updates `changeSignal` when main signal changes

4. **Computed signals** - Derive percentage, prefix, icon from the change

The effect uses `peek()` to read the previous current value without creating a circular dependency:

```java
Signal.effect(this, () -> {
    double current = signal.get();      // Creates dependency
    double previous = changeSignal.peek().current();  // No dependency
    changeSignal.set(new Change(previous, current));
});
```

If we used `changeSignal.get()` instead of `peek()`, the effect would re-run whenever `changeSignal` changes, creating an infinite loop since the effect itself changes `changeSignal`.

### <a id="using-component-binding-methods"></a>Using Component Binding Methods

The highlight card also demonstrates using component binding methods instead of manually creating and updating components with setters. Notice how components are bound to signals using `bindText()` and constructor-based binding:

```java
// Text binding via bindText()
Span valueSpan = new Span();
valueSpan.bindText(signal.map(format::apply));

Badge badge = new Badge();
badge.bindText(prefixSignal.map(prefix -> prefix + percentageSignal.get()));

// Icon constructor accepts Signal<VaadinIcon>
Icon icon = new Icon(iconSignal);

// Theme list binding via bind()
badge.getElement().getThemeList().bind("success", successSignal);
badge.getElement().getThemeList().bind("error", Signal.not(successSignal));
```

These binding methods are preferred over manually creating and updating components with setters because:

- **Less boilerplate** - No need to write `Signal.effect(component, () → …​)` or manual setter calls in response to state changes

- **Clearer intent** - The binding method name clearly states what’s being bound

- **Built-in lifecycle** - The framework manages effect cleanup when components are detached

- **Type safety** - Binding methods are type-checked at compile time

- **Automatic updates** - Component state stays in sync automatically; no need to manually call `setText()`, `setVisible()`, etc.

Use `bindText()`, `bindValue()`, `bindVisible()`, and similar methods whenever available. Create manual effects only when you need custom logic that isn’t covered by built-in binding methods.

## <a id="key-patterns-summary"></a>Key Patterns Summary

**Service callback pattern**

```java
// Service calls callback with plain data
schedulerService.scheduleDashboardDataUpdate(this::onDataUpdate);

// Callback only updates signals
private void onDataUpdate(DashboardData data) {
    currentUsersSignal.set(data.currentUsers());
    // ... more signal updates
}
```

**Effect-based chart updates**

```java
// Effect watches signal and updates series
Signal.effect(chart, () -> {
    series.setData(signal.get().stream()
        .map(Signal::get)
        .toArray(Number[]::new));
});
```

**Coordinated multi-signal effects**

```java
// Effect watches multiple signals
Signal.effect(chart, () -> {
    signal1.get();
    signal2.get();
    signal3.get();
    chart.drawChart();
});
```

**Change tracking with peek()**

```java
// Effect tracks previous value without circular dependency
Signal.effect(component, () -> {
    double current = signal.get();
    double previous = changeSignal.peek().current();
    changeSignal.set(new Change(previous, current));
});
```

## <a id="best-practices"></a>Best Practices

**Store all reactive state in signals** - Don’t mix signals with regular fields for state. Make it clear what’s reactive.

**Keep callbacks focused on signals** - Service callbacks should only update signals, never manipulate UI directly.

**Use effects for UI updates** - Let effects watch signals and update components. Don’t manually trigger updates.

**Separate concerns with multiple effects** - Don’t create one giant effect. Split into focused effects with single responsibilities.

**Use peek() to avoid circular dependencies** - When reading a signal you’re about to update inside an effect, use `peek()` not `get()`.

**Prefer binding methods over manual effects** - Use `bindText()`, `bindValue()`, `bindVisible()`, and component constructors that accept signals when available.

**Enable Vaadin Push feature for real-time updates** - Without [Vaadin Push](https://vaadin.com/docs/next/flow/advanced/server-push.md), signal changes only propagate when users interact with the UI.

## <a id="summary"></a>Summary

This dashboard demonstrates a clean signal-based architecture:

- Backend services work with plain data objects, not signals

- A callback layer updates signals from service data

- Effects watch signals and update UI automatically

- Component binding methods (`bindText()`, constructors) preferred over manual component creation/updates with setters

- No manual state management, listeners, or UI.access() calls needed

- Charts update via separate effects for different concerns

The result is maintainable, testable code with automatic UI synchronization.
