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
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Signals hold the raw source of truth while computed values derive everything else automatically.
- 2Keeping updates immutable lets Angular detect changes and re-run only the affected computeds.
- 3An effect turns any reactive read into a side effect, like persisting state whenever it changes.
Related explainers
typescript
import { NestFactory } from '@nestjs/core'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import { ValidationPipe } from '@nestjs/common'; import { ApiProperty } from '@nestjs/swagger';
Wiring validation and Swagger docs in NestJS
validation
openapi
decorators
Intermediate
8 steps
typescript
import { Component, Input } from '@angular/core'; interface Order { id: string;
How Angular ICU plurals localize an order summary
i18n
pluralization
standalone-component
Intermediate
8 steps
ruby
class KeyTransformer def self.camelize(data) new.camelize(data) end
Recursively camelizing nested Ruby data
recursion
data-transformation
pattern-matching
Intermediate
7 steps
typescript
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'; import { Observable, catchError, concatMap, finalize } from 'rxjs'; import { DataSource, QueryRunner } from 'typeorm';
Wrapping requests in a transaction with NestJS
interceptors
transactions
rxjs
Advanced
7 steps
typescript
type CsvColumn<T> = { header: string; value: (row: T) => string | number | boolean | null | undefined; };
Building a type-safe CSV writer in TypeScript
generics
serialization
escaping
Intermediate
7 steps
typescript
import { parsePhoneNumberFromString, CountryCode } from 'libphonenumber-js'; export interface NormalizedPhone { e164: string;
Normalizing phone numbers to E.164 in TypeScript
validation
normalization
error-handling
Intermediate
7 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/a-signal-based-cart-store-in-angular-explained-typescript-43d9/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.