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 { registerLocaleData } from '@angular/common'; import localeFr from '@angular/common/locales/fr'; import localeFrExtra from '@angular/common/locales/extra/fr'; import localeDe from '@angular/common/locales/de';
Locale-aware bootstrapping in Angular
i18n
localization
dependency-injection
Intermediate
8 steps
typescript
import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import * as Joi from 'joi';
Validating env config at boot in NestJS
configuration
schema-validation
environment-variables
Intermediate
8 steps
typescript
import { Inject, Injectable, Logger } from '@nestjs/common'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { Cache } from 'cache-manager'; import { InjectRepository } from '@nestjs/typeorm';
A cache-aside country lookup in NestJS
cache-aside
dependency-injection
batch-lookup
Intermediate
8 steps
typescript
import { Injectable, effect, signal, computed } from '@angular/core'; interface Preferences { theme: 'light' | 'dark';
A signal-based preferences store in Angular
signals
state-management
persistence
Intermediate
7 steps
typescript
import { useEffect, useState } from "react"; interface Section { id: string;
Building a scroll-spy hook in React
custom-hooks
intersectionobserver
dom-observation
Intermediate
8 steps
go
package logging import ( "context"
Deduplicating log attributes in Go's slog
decorator-pattern
structured-logging
immutability
Intermediate
8 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.