typescript 68 lines · 8 steps

A signal-based cart store in Angular

How Angular signals build a reactive shopping cart with derived totals and automatic persistence.

Explained by highlit
1import { Injectable, computed, effect, signal } from '@angular/core';
2 
3export interface CartLine {
4 id: string;
5 name: string;
6 unitPrice: number;
7 quantity: number;
8}
9 
10@Injectable({ providedIn: 'root' })
11export class CartStore {
12 private readonly lines = signal<CartLine[]>([]);
13 private readonly discountCode = signal<string | null>(null);
14 
15 readonly items = this.lines.asReadonly();
16 
17 readonly itemCount = computed(() =>
18 this.lines().reduce((total, line) => total + line.quantity, 0),
19 );
20 
21 readonly subtotal = computed(() =>
22 this.lines().reduce((total, line) => total + line.unitPrice * line.quantity, 0),
23 );
24 
25 readonly discount = computed(() => {
26 const code = this.discountCode();
27 if (code === 'SAVE10') return this.subtotal() * 0.1;
28 if (code === 'FREESHIP') return Math.min(this.subtotal(), 9.99);
29 return 0;
30 });
31 
32 readonly tax = computed(() => (this.subtotal() - this.discount()) * 0.0825);
33 
34 readonly total = computed(() =>
35 Math.max(0, this.subtotal() - this.discount() + this.tax()),
36 );
37 
38 constructor() {
39 effect(() => {
40 localStorage.setItem(
41 'cart',
42 JSON.stringify({ lines: this.lines(), discountCode: this.discountCode() }),
43 );
44 });
45 }
46 
47 addLine(line: CartLine): void {
48 this.lines.update((lines) => {
49 const existing = lines.find((l) => l.id === line.id);
50 if (!existing) return [...lines, line];
51 return lines.map((l) =>
52 l.id === line.id ? { ...l, quantity: l.quantity + line.quantity } : l,
53 );
54 });
55 }
56 
57 setQuantity(id: string, quantity: number): void {
58 this.lines.update((lines) =>
59 quantity <= 0
60 ? lines.filter((l) => l.id !== id)
61 : lines.map((l) => (l.id === id ? { ...l, quantity } : l)),
62 );
63 }
64 
65 applyDiscount(code: string): void {
66 this.discountCode.set(code.trim().toUpperCase());
67 }
68}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Signals hold the raw source of truth while computed values derive everything else automatically.
  2. 2Keeping updates immutable lets Angular detect changes and re-run only the affected computeds.
  3. 3An effect turns any reactive read into a side effect, like persisting state whenever it changes.

Related explainers

Share this explainer

Here's the card — post it anywhere.

A signal-based cart store in Angular — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code