A WritableSignal<T> is an Angular signal whose value represents mutable application state. Create one with signal<T>(initialValue), read it by invoking it as a function, and change it through writable operations rather than assignment. section 10.4.1
Choose the operation that matches the kind of change:
- set(value) replaces the signal’s current value. section 10.4.1
- update(function) receives the current value and returns a new value. section 10.4.1
- mutate(function) receives the current value and modifies it in place, making it useful for incremental array or object changes. section 6.2.1 section 10.4.1
- asReadonly() returns a Signal<T> that can be read and used by computed signals but cannot be modified directly. section 10.4.1
Encapsulating cart state
A Cart service stores its product selections in a private WritableSignal<CartLine[]> initialized with signal([]). Methods such as addLine and removeLine use mutate to apply changes. A lines getter returns linesSignal.asReadonly(), so the Cart controls every modification while components can read the selections and derive summaries with computed. section 6.2.1
Changing a displayed index
private index = signal<number>(0); changes the initial value from a plain number to writable state. A toggle operation can call index.update(currentVal => (currentVal + 1) % 2), while templates read the value with index(). section 10.4.1
Listing 6.1. The contents of the cart.model.ts file in the src/app/model folder
import { Injectable, Signal, WritableSignal, computed, signal }
from "@angular/core";
import { Product } from "./product.model";
@Injectable()
export class Cart {
private linesSignal: WritableSignal<CartLine[]>;
public summary: Signal<CartSummary>;
constructor() {
this.linesSignal = signal([]);
this.summary = computed(() => {
let newSummary = new CartSummary();
this.linesSignal().forEach(l => {
newSummary.itemCount += l.quantity;
newSummary.cartPrice += l.lineTotal;
});
return newSummary;
})
}
get lines(): Signal<CartLine[]> {
return this.linesSignal.asReadonly();
}
addLine(product: Product, quantity: number = 1) {
this.linesSignal.mutate(linesArray => {
let line = linesArray.find(l => l.product.id == product.id);
if (line != undefined) {
line.quantity += quantity;
} else {
linesArray.push(new CartLine(product, quantity));
}
});
}
updateQuantity(product: Product, quantity: number) {
this.linesSignal.mutate(linesArray => {
let line = linesArray.find(l => l.product.id == product.id);
if (line != undefined) {
line.quantity = Number(quantity);
}
});
}
removeLine(id: number) {
this.linesSignal.mutate(linesArray => {
let index = linesArray.findIndex(l => l.product.id == id);
linesArray.splice(index, 1);
});
}
clear() {
this.linesSignal.set([]);
}
}
export class CartLine {
constructor(public product: Product,
public quantity: number) {}
get lineTotal() {
return this.quantity * (this.product.price ?? 0);
}
}
export class CartSummary {
itemCount: number = 0;
cartPrice: number = 0;
}
Inspect a complete Cart model showing private writable state, mutate-based operations, a read-only lines view, and a computed summary. section 6.2.1
Writable signals describe relationships between mutable values so Angular can update affected bindings without evaluating unrelated template expressions. Use computed signals for derived values, and avoid using effects to modify writable signals or other application state. section 10.6 section 10.4.3

