Sitemap

Building Space Donuts with Angular 22 — Part 6: Building the Shopping Cart and Understanding Computed Signals

6 min readOct 28, 2026

--

In the previous article, we introduced NgRx SignalStore to manage the shopping cart state centrally.

We can now add donuts, update quantities, and access the cart from different components.

It's time to build the shopping cart UI.

In this article, we'll display the selected products in an overlay, allow users to adjust their quantities, and calculate the total price.

Along the way, we'll take a closer look at one of Angular's most useful reactive primitives: computed().

1. What Should the Shopping Cart Display?

For our Space Donuts application, the shopping cart should provide a clear overview of the selected products.

Each row displays:

  • A small product image on the left.
  • The donut's title.
  • A quantity selector.
  • The price for that item's current quantity.

At the bottom, we'll display the total price of all items.

Our existing CartStore already contains the data and methods we need. The overlay doesn't need to maintain a separate copy of the cart.

2. Creating the Cart Overlay

We'll keep the generic overlay component in our shared directory and create a feature-specific CartOverlay inside the cart directory.

This separation is useful because the overlay handles reusable behavior, such as displaying a backdrop and closing the panel, while the cart overlay is responsible for shopping cart content.

Our CartOverlay injects the store:

readonly cartStore = inject(CartStore);

It also needs to calculate the total price.

Before implementing that calculation, let's look at what the UI needs to display.

3. Displaying the Cart Items

Our template iterates over the items stored in the cart:

<div class="cart-items">
@for (item of cartStore.items(); track item.donut.id) {
<div class="cart-item">
<img
class="cart-item-image"
[src]="item.donut.image"
[alt]="item.donut.title"
/>

<h3 class="cart-item-title">
{{ item.donut.title }}
</h3>

<app-quantity-selector
[quantity]="item.quantity"
(increment)="cartStore.increaseQuantity(item.donut.id)"
(decrement)="cartStore.decreaseQuantity(item.donut.id)"
/>

<span class="cart-item-price">
{{ item.donut.price * item.quantity | currency:'EUR' }}
</span>
</div>
}
</div>
Press enter or click to view image in full size
shopping cart overlay with donut items

Let's examine a few details.

Tracking items by ID

@for (item of cartStore.items(); track item.donut.id)

Each donut has a unique ID, so Angular can identify individual items when the list changes.

This helps Angular efficiently update the rendered list instead of unnecessarily recreating DOM elements.

Calculating the price of a single cart item

{{ item.donut.price * item.quantity | currency:'EUR' }}

Suppose a donut costs €3.50 and the user selects three of them.

The line total is:

€3.50 × 3 = €10.50

The expression calculates that amount directly in the template.

For a small, straightforward calculation like this, the expression is easy to understand. More complex or reusable calculations may deserve their own computed signal or dedicated method.

4. Calculating the Total Price with computed()

Now we come to the interesting part 🎉.

We want to display the total price of all products in the shopping cart.

A first approach might be to calculate the price whenever we need it:

calculateTotalPrice(): number {
return this.cartStore.items().reduce((total, item) => {
return total + item.donut.price * item.quantity;
}, 0);
}

We could call this method in the template:

{{ calculateTotalPrice() | currency:'EUR' }}

However, Angular may evaluate template expressions during change detection. A method used in a template can therefore be called repeatedly, even when the underlying cart data hasn't changed.

For a small shopping cart, this particular calculation is inexpensive. Nevertheless, we have a better option for a value that depends on reactive state: computed().

Creating the computed signal

In cart-overlay.ts, we can write:

readonly totalPrice = computed(() =>
this.cartStore.items().reduce((total, item) => {
return total + item.donut.price * item.quantity;
}, 0)
);

Let's break this down.

Get Torben Gabriel’s stories in your inbox

Join Medium for free to get updates from this writer.

First, this.cartStore.items() reads the current cart items.

Next, reduce() iterates over the array and calculates the sum of every item's line total:

item.donut.price * item.quantity

The initial accumulator value is zero:

}, 0)

Finally, computed() turns the calculation into a derived signal.

The result: total price due

Why is this better than a regular method?

The main advantage isn't simply that the code looks cleaner.

A computed signal tracks the signals it reads. When one of those dependencies changes, Angular invalidates the cached result. The next time the computed value is read, Angular recalculates it.

If the dependencies haven't changed, the previous result can be reused.

This is called memoization.

computed: is memoized. The value is cached and is only recalculated if a signal that was read during the last run changes. The calculation is also lazy: it runs only when the value is first read.

In our example, the total price depends on the cart items:

this.cartStore.items()

When a donut is added, removed, or updated, the store produces a new items array. That change invalidates totalPrice, and the next read produces the updated sum.

If unrelated application state changes without changing the cart items, the computed total doesn't need to be recalculated.

The important distinction is that computed() doesn't run a calculation continuously in the background. It derives a value from its dependencies and caches the result until those dependencies change.

5. Displaying the Total Price

Now that we have the computed signal, displaying the total is straightforward.

Add a footer below the list of cart items:

<div class="cart-footer">
<span class="cart-total-label">
Total
</span>

<span class="cart-total-price">
{{ totalPrice() | currency:'EUR' }}
</span>
</div>

Notice the parentheses:

totalPrice()

A computed signal is read by calling it like a function.

Angular tracks that read in the template, so the UI updates when the computed value changes.

We can style the footer to separate it visually from the list:

.cart-footer {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;

margin-top: 1rem;
padding-top: 1.25rem;

border-top: 1px solid #f0e4ed;
}

.cart-total-label {
font-size: 1.1rem;
font-weight: 600;
}

.cart-total-price {
font-size: 1.25rem;
font-weight: 700;
white-space: nowrap;
}

The footer sits outside the @for loop, so it appears only once, regardless of how many products the cart contains.

6. Why Not Store the Total Price Directly?

You might wonder why we don't simply store totalPrice alongside the cart items.

For example:

interface CartState {
items: CartItem[];
totalPrice: number;
}

The problem is that totalPrice is entirely determined by the items and their quantities.

If we store both values independently, every operation that changes the cart must also update the total price correctly.

Whenever a donut is added, removed, or updated, we would need to ensure both pieces of state remain synchronized.

If one update forgets to recalculate the total, the UI could display an incorrect amount.

With computed(), we avoid this problem.

We store the source data:

items: CartItem[]

And derive the total:

readonly totalPrice = computed(() =>
this.cartStore.items().reduce((total, item) => {
return total + item.donut.price * item.quantity;
}, 0)
);

This follows an important principle of reactive application design:

Store the state you need to maintain, and derive values that can be calculated from that state.

There is no need to synchronize a second value when Angular can derive it automatically.

Conclusion

Our shopping cart now displays the selected donuts, allows users to change quantities, and calculates the total price automatically.

More importantly, we've seen how Angular's computed() function helps us keep derived state consistent.

Rather than storing and manually updating the total price, we calculate it from the cart items and let Angular manage the reactive dependency.

This keeps the implementation simple, predictable, and easy to extend.

In the next part of our Space Donuts series, we'll continue exploring Angular 22 and look at another aspect of building maintainable applications.

Source code: Space Donuts on GitHub

--

--