
In the first two parts of this series, we introduced Angular Signals and built a reactive shopping cart. Our CartService
already supports core operations like adding, removing, and clearing items, as well as computing total price and item count using computed()
. All of this was done without touching RxJS, subscriptions, or change detection hacks.
But a real-world cart does more than tally up numbers. It validates inventory, applies discount codes, persists data across sessions, and integrates cleanly with monitoring and debugging tools.
In this third and final article, we’ll extend our cart with production-ready features and show how Signals not only simplify reactivity, but also enhance observability, performance, and developer experience.
We'll introduce Angular’s effect()
function to perform side effects like syncing to localStorage
. We’ll handle derived state conditions like discount logic and stock checks. And we’ll wrap up by reviewing the final architecture to see how everything fits together cleanly.
Applying Discounts with Reactive Computed State
The ability to apply a discount code is a common cart feature. We’ll implement this by tracking the current code as a signal()
, and then deriving the discountAmount
and finalTotal
values based on that code and the cart's total.
First, we'll add a new signal to the CartService:
_discountCode = signal<string | null>(null);
Then expose a method to update it:
applyDiscount(code: string) { this._discountCode.set(code); }
Next, we define a reactive computed value for the discount amount.
Listing 1: Computing the Discount Amount
Let’s assume a simple business rule: "SAVE10"
gives 10% off, and other codes give nothing.
readonly discountAmount = computed(() => { const code = this._discountCode(); const total = this.total(); if (code === 'SAVE10') { return total * 0.1; } return 0; });
From this, we derive the final price the user will pay:
readonly finalTotal = computed(() => this.total() - this.discountAmount());
This is what makes Signals so powerful: you model real business logic as pure reactive expressions. If the cart total or the discount code changes, these values update immediately and automatically. There’s no need to re-run methods or patch together combineLatest
chains.
Validating Inventory in Real Time
Another crucial feature is validating whether items in a cart are still in stock. Let’s assume we fetch the current inventory from a product service and expose it as a signal:
readonly inventory = signal<Record<string, number>>({});
We can inject this ProductService
into the CartService
and reactively compute whether the cart is valid.
Listing 2: Inventory Validation with computed()
This reactive check becomes incredibly useful:
readonly isValid = computed(() => { const items = this._items(); const stock = this.productService.inventory(); return items.every(item => { const available = stock[item.id] ?? 0; return item.qty <= available; }); });
You can disable the checkout button, show warnings, or highlight out-of-stock items; all with zero manual subscriptions or additional logic.
And because it’s computed, the check only re-evaluates when either _items
or inventory
changes. This avoids unnecessary work and fits naturally into Angular’s change detection strategy.
Persisting the Cart to localStorage
with effect()
One of the most practical use cases for effect()
is syncing state to localStorage
. Whenever the cart changes, we want to serialize it and persist it.
Listing 3: Saving Cart State with effect()
We can do this by using an effect in the CartService
constructor:
constructor() { effect(() => { const items = this._items(); localStorage.setItem('cart', JSON.stringify(items)); }); }
That’s it. This runs automatically whenever _items()
changes. Because effect()
tracks dependencies on first execution, Angular knows to re-run this logic only when needed. There’s no need to debounce, throttle, or manage cleanup.
To restore the cart on load, you can hydrate it like this:
private hydrateFromStorage() { const saved = localStorage.getItem('cart'); if (saved) { try { const parsed = JSON.parse(saved); this._items.set(parsed); } catch { // ignore invalid storage state } } }
Call this once from the constructor, before registering the effect.
This kind of logic is normally scattered across lifecycle hooks or abstracted into effect libraries. With Angular’s native Signals, it becomes first-class and clean.
Debugging is Easier with Signals
One of the hidden costs of observables is their runtime complexity. When something breaks, you’re often stuck digging through custom pipes, nested subscriptions, and unclear emission order.
With Signals, the control flow is easier to follow. Everything is synchronous, declarative, and traceable by reading code. You can log Signal values directly without worrying about emissions or side effects.
Listing 4: Adding a Debugging Effect
Let's see how we can add a debugging effect:
constructor() { effect(() => { console.log('Cart updated:', this._items()); console.log('Total:', this.finalTotal()); console.log('Discount:', this.discountAmount()); }); }
Because these reads are reactive, you’ll only see logs when the values actually change. There’s no need to guess when a stream will emit, or worry about memory leaks from dangling subscriptions.
And since Signals work naturally with OnPush
change detection, you won’t find yourself poking at ChangeDetectorRef.markForCheck()
or async
pipe timing issues. Your app updates when, and only when, it needs to.
Performance: Fine-Grained Recalculation
Let’s talk about performance. In large apps, it’s not enough to be reactive; you need to be selective. You want updates to happen only when something relevant has changed.
Signals achieve this through fine-grained dependency tracking. If a computed value depends on three signals, it only re-runs when one of those three actually changes. Angular tracks these dependencies at runtime, during the first execution of the function.
This means derived state like discountAmount
and finalTotal
doesn't recompute unless the total or the discount code changes. Unlike template expressions, which are re-evaluated on every change-detection cycle (unless the cycle is skipped for that component by OnPush
), Signals re-evaluate only when an upstream dependency updates.
This behavior scales beautifully. You can derive pricing summaries, cart warnings, button states, and UI fragments without adding performance penalties.
And since the update model is pull-based, you avoid unnecessary emissions when nothing has changed. You get full reactivity, without the noise.
Final Architecture Review
Let’s take a step back and review what we’ve built across the series.
Our final CartService
now handles:
- Reactive state with
signal()
for cart items and discount codes - Derived values like total, count, discount amount, and final total
- Business rules like inventory checks using injected services
- Persistence to
localStorage
witheffect()
- Testability without subscriptions or marble syntax
- Observability with simple logging or hooks
All of this exists in a single service. No store, no reducer maps, no action types, and no boilerplate. Just direct, reactive logic built on Angular’s own primitives.
Even better, the entire cart architecture remains template-friendly, OnPush-compatible, and future-proof. You can easily extend it to support quantity editing, wishlist features, or checkout flows all using the same architectural pattern.
This approach encourages clear boundaries, functional state updates, and encapsulated logic: all of which align with scalable frontend development.
Conclusion: Better State, Better Apps
In this series, we built a complete, scalable shopping cart using nothing but Angular Signals. No external store libraries. No RxJS overhead — not even a single observable. Just modern Angular, used to its full potential.
Angular Signals are more than just a syntactic shift, they’re a new mental model. One that moves away from passive data streams and toward synchronous, transparent, and fine-grained reactivity.
By using Signals in your services, you gain complete control over your app’s state logic without sacrificing testability, observability, or performance. You write less code, but more meaningful code. You ship features faster, with fewer bugs. And when things go wrong, you can trace what happened just by reading your service.
If you're using monitoring tools, Signals give you a much clearer picture of what changed, when it changed, and why downstream effects occurred. Combined with effect()
and Angular’s runtime hooks, this opens the door to incredibly powerful diagnostics and logging, without extra dependencies.
Now imagine what else you could build.
Happy coding!
Wondering what you can do next?
Finished this article? Here are a few more things you can do:
- Subscribe to our JavaScript Sorcery newsletter and never miss an article again.
- Start monitoring your JavaScript app with AppSignal.
- Share this article on social media
Most popular Javascript articles
Top 5 HTTP Request Libraries for Node.js
Let's check out 5 major HTTP libraries we can use for Node.js and dive into their strengths and weaknesses.
See moreWhen to Use Bun Instead of Node.js
Bun has gained in popularity due to its great performance capabilities. Let's see when Bun is a better alternative to Node.js.
See moreHow to Implement Rate Limiting in Express for Node.js
We'll explore the ins and outs of rate limiting and see why it's needed for your Node.js application.
See more

Sonu Kapoor
Guest author Sonu Kapoor is a seasoned Senior Software Engineer and internationally recognized speaker with over two decades of experience in building high-performance web applications. He is a Microsoft MVP (2005–2010, 2024) and a Google Developer Expert for Angular, as well as a trusted contributor to the Angular Framework, where he has helped shape the future of frontend development. Besides that, he has published two books.
All articles by Sonu KapoorBecome our next author!
AppSignal monitors your apps
AppSignal provides insights for Ruby, Rails, Elixir, Phoenix, Node.js, Express and many other frameworks and libraries. We are located in beautiful Amsterdam. We love stroopwafels. If you do too, let us know. We might send you some!
