Docs

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

Shopping Cart Example

Building a reactive shopping cart with signals that automatically updates totals and manages dynamic lists of items.

This example demonstrates building a reactive shopping cart with automatic total calculations. It shows how to manage dynamic lists of items and efficiently update the UI when items are added, removed, or modified.

Overview

The shopping cart tracks:

  • Cart items with quantities

  • Discount codes

  • Shipping options

  • Real-time totals (subtotal, discount, tax, shipping, and grand total)

All calculations update automatically as users modify the cart, apply discounts, or change shipping options.

State Management with Signals

The cart uses three signals to track its state:

Source code
ShoppingCartSignals.java
ListSignal<CartItem>

Manages the list of cart items. Each item in the list is itself a signal (ValueSignal<CartItem>), enabling reactive updates when quantities change.

ValueSignal<String>

Tracks the discount code entered by the user.

ValueSignal<ShippingOption>

Tracks the selected shipping method.

Computed Totals

Each total is a computed signal that automatically recalculates when dependencies change.

Subtotal

The subtotal sums the price of all items in the cart:

Source code
ShoppingCartSignals.java

This computed signal reads from cartItemsSignal. It accesses each item’s signal value using map(ValueSignal::get), then multiplies the product price by quantity. The result is summed using reduce().

When cart items are added, removed, or quantities change, this signal recalculates automatically.

Discount

The discount depends on both the discount code and the subtotal:

Source code
ShoppingCartSignals.java

This computed signal reads from both discountCodeSignal and subtotalSignal. When either changes, the discount recalculates.

Tax

Tax is calculated on the discounted subtotal:

Source code
ShoppingCartSignals.java

This demonstrates chaining computed signals. The tax signal depends on subtotalSignal and discountSignal, both of which are themselves computed signals.

Shipping

Shipping cost depends on the selected shipping option:

Source code
ShoppingCartSignals.java

Grand Total

The final total combines all computed values:

Source code
ShoppingCartSignals.java

This computed signal depends on four other computed signals: subtotalSignal, discountSignal, shippingSignal, and taxSignal. When any of these changes, the total updates automatically.

Binding Totals to UI

Each total is bound to a label that displays it:

Source code
ShoppingCartSignals.java

Key techniques:

bindText()

Updates the label text whenever the signal changes

map()

Transforms the signal value (e.g., formatting currency)

bindVisible()

Shows the discount label only when a discount is applied

Form Field Binding

The discount code and shipping option use two-way binding:

Source code
ShoppingCartSignals.java
Source code
ShoppingCartSignals.java

The bindValue(signal, signal::set) method creates two-way binding:

  • When the user types in the field, the write callback (signal::set) updates the signal

  • When the signal is updated programmatically, the framework reads via signal.get() and updates the field

This is different from read-only bindings like bindText(), which only update the UI when the signal changes.

Dynamic List Rendering with Children Binding

The core of the shopping cart is rendering the list of cart items. The bindChildren() method on any container component efficiently manages this:

Source code
ShoppingCartSignals.java

bindChildren() takes two parameters:

  1. ListSignal: The signal containing the list of items

  2. Factory function: A function that creates a component for each item signal

The factory function receives a ValueSignal<CartItem> for each item and creates the component once. The component then binds to this signal to receive updates. Crucially, component instances are created only when new items are added to the list. When item data changes (such as quantity updates), the existing component updates through its signal bindings rather than being recreated. This approach minimizes memory allocation and DOM operations: components are reused when items are reordered, detached only when items are removed, and their internal state updates reactively without component recreation.

Creating Cart Item Rows

Each cart item is rendered as a row with reactive bindings:

Source code
ShoppingCartSignals.java

Key techniques used:

Two-way signal mapping

itemSignal.map(CartItem::quantity, CartItem::withQuantity) creates a writable signal that reads the quantity using get() and writes it back using the withQuantity method via set(). This enables two-way binding between the quantity field and the cart item.

Nested computed signal

Signal.computed() calculates the item total by multiplying price by quantity. This computed signal updates automatically when the quantity changes.

Removing items

Calling cartItemsSignal.remove(itemSignal) removes the item from the list. The component is automatically detached from the DOM.

See Two-Way Signal Mapping for more details on mapping signals to nested properties.

Adding Items to Cart

When a user adds a product, the cart checks if it’s already present:

Source code
ShoppingCartSignals.java

This logic:

  1. Searches the cart for an item with the same product ID using signal.get()

  2. If found, increments its quantity by updating the signal’s value using signal.set()

  3. If not found, inserts a new cart item at the end of the list

When the quantity is updated on an existing item, the component row updates automatically without being recreated. Only the quantity field and item total update.

Reactivity Flow

Here’s what happens when a user changes a quantity:

  1. User types in the quantity field

  2. The two-way mapped signal updates the CartItem value

  3. The item total computed signal recalculates

  4. The subtotal computed signal recalculates

  5. The discount computed signal recalculates (because it depends on subtotal)

  6. The tax computed signal recalculates (because it depends on subtotal and discount)

  7. The grand total computed signal recalculates (because it depends on all other totals)

  8. All bound labels update automatically

All of this happens automatically without manual event listeners or state synchronization.

When to Use ListSignal

ListSignal is ideal for:

  • Single-user scenarios like this shopping cart

  • Dynamic form fields (add/remove rows)

  • Local selections and filters

  • Any list where items are added, removed, or reordered

For multi-user scenarios where multiple users need to see the same list in real-time (like a collaborative task board), use SharedListSignal instead. See Shared Signals for details.

Key Takeaways

ListSignal

Manages dynamic lists with per-item reactivity. Each item is a ValueSignal that can be updated independently.

bindChildren()

Efficiently renders components from a list signal. Available on any container component (HasComponents), components are added, removed, and reordered without unnecessary recreation.

Two-way mapping

signal.map(getter, merger) creates writable signals for nested properties, enabling direct binding to form fields.

Computed signal chains

Complex calculations can be broken into multiple computed signals that depend on each other. Changes propagate automatically through the chain.

Automatic updates

When any signal changes, all dependent computed signals and bindings update automatically without manual synchronization.