From harness-claude
Manage reactive state with Angular Signals — signal(), computed(), effect(), and toSignal() — for fine-grained, zone-free reactivity
How this skill is triggered — by the user, by Claude, or both
Slash command
/harness-claude:angular-signals-patternThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
> Manage reactive state with Angular Signals — signal(), computed(), effect(), and toSignal() — for fine-grained, zone-free reactivity
Manage reactive state with Angular Signals — signal(), computed(), effect(), and toSignal() — for fine-grained, zone-free reactivity
BehaviorSubject + async pipe patterns with simpler signal-based statetoSignal()signal<T>(initialValue). The returned WritableSignal<T> exposes .set(), .update(), and .mutate() (arrays/objects).computed(() => ...). Computed signals are lazy and memoized — they only recompute when their dependencies change. Never compute inside a template expression; use computed() instead.effect(() => ...). Effects re-run automatically when any signal they read changes. Clean up resources by returning a cleanup function or using the onCleanup callback.Observable to a signal with toSignal(obs$, { initialValue: ... }). This subscribes for you and unsubscribes on destroy. Provide initialValue to avoid the undefined initial state.toObservable(sig) when you need to compose it with RxJS operators.input()) over @Input() for new components — they integrate with the reactivity graph natively..set() or .update() inside a computed() — computed signals must be pure.import { Component, signal, computed, effect, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { ProductService } from './product.service';
@Component({
selector: 'app-cart',
template: `
<p>Items: {{ itemCount() }}</p>
<p>Total: {{ formattedTotal() }}</p>
<button (click)="addItem(selectedProduct())">Add</button>
`,
})
export class CartComponent {
private productService = inject(ProductService);
// Convert observable to signal — auto-unsubscribed on destroy
selectedProduct = toSignal(this.productService.selected$, {
initialValue: null,
});
items = signal<CartItem[]>([]);
itemCount = computed(() => this.items().length);
total = computed(() => this.items().reduce((sum, item) => sum + item.price * item.qty, 0));
formattedTotal = computed(() =>
new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(this.total())
);
constructor() {
// Side effect: persist cart to localStorage whenever items change
effect(() => {
localStorage.setItem('cart', JSON.stringify(this.items()));
});
}
addItem(product: Product | null): void {
if (!product) return;
this.items.update((items) => [...items, { ...product, qty: 1 }]);
}
}
Signal vs BehaviorSubject: A BehaviorSubject requires .subscribe(), .next(), and .unsubscribe() (or takeUntil). A WritableSignal has no subscription overhead and integrates with Angular's change detection graph directly. Signals also compose with computed() without the combineLatest ceremony required by observables.
Lazy computation: computed() is lazy and cached. If no consumer reads the computed signal, it never runs. If the dependencies haven't changed since last read, the cached value is returned without re-running the function. This makes computed signals safe to use in templates even for expensive derivations.
Effect cleanup: Effects that set up subscriptions, timers, or DOM listeners should clean up on re-run:
effect((onCleanup) => {
const id = setInterval(() => this.tick.update((t) => t + 1), 1000);
onCleanup(() => clearInterval(id));
});
toSignal guarantees: toSignal() must be called in an injection context (constructor or field initializer). It auto-subscribes and auto-unsubscribes using DestroyRef. The initialValue option avoids the T | undefined type widening; requireSync: true can be used when the observable is known to emit synchronously (e.g., BehaviorSubject).
Mutation helpers: For arrays and objects, use .update() to apply a pure transform:
this.items.update((list) => list.filter((i) => i.id !== removedId));
Avoid mutating in place then calling .set(this.items()) — signal equality checks use reference equality, so this won't trigger updates.
Zoneless change detection: Angular 18+ supports provideExperimentalZonelessChangeDetection(). With signals, components no longer need Zone.js to trigger change detection — signal writes schedule DOM updates directly. Adopting signals now future-proofs components for zoneless.
When to keep RxJS: Signals are not a replacement for RxJS when you need time-based operators (debounceTime, throttleTime), combination operators (combineLatest, forkJoin), or error handling (catchError, retry). Bridge with toSignal() / toObservable() at the boundary.
https://angular.dev/guide/signals
npx claudepluginhub intense-visions/harness-engineering --plugin harness-claudeGuides Angular Signals usage for fine-grained reactive state management and zone-less change detection in Angular 16+ applications.
Guides Angular state management with Signals, NgRx, and RxJS for local, global, and server state. Helps choose patterns and migrate from legacy approaches.
Provides Angular 14+ expertise for standalone components, signals, RxJS patterns, NgRx state management, dependency injection, lazy loading routes, OnPush change detection, and testing.