install
source · Clone the upstream repo
git clone https://github.com/Intense-Visions/harness-engineering
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/Intense-Visions/harness-engineering "$T" && mkdir -p ~/.claude/skills && cp -r "$T/agents/skills/claude-code/angular-signals-pattern" ~/.claude/skills/intense-visions-harness-engineering-angular-signals-pattern && rm -rf "$T"
manifest:
agents/skills/claude-code/angular-signals-pattern/SKILL.mdsource content
Angular Signals Pattern
Manage reactive state with Angular Signals — signal(), computed(), effect(), and toSignal() — for fine-grained, zone-free reactivity
When to Use
- Building new components in Angular 17+ that need reactive local state
- Replacing
+BehaviorSubject
pipe patterns with simpler signal-based stateasync - Deriving display values from multiple state pieces without manual subscription management
- Bridging RxJS observables into signal-based components via
toSignal() - Preparing for zoneless change detection (Angular 18+)
Instructions
- Create mutable state with
. The returnedsignal<T>(initialValue)
exposesWritableSignal<T>
,.set()
, and.update()
(arrays/objects)..mutate() - Derive values with
. Computed signals are lazy and memoized — they only recompute when their dependencies change. Never compute inside a template expression; usecomputed(() => ...)
instead.computed() - Run side effects with
. Effects re-run automatically when any signal they read changes. Clean up resources by returning a cleanup function or using theeffect(() => ...)
callback.onCleanup - Convert an RxJS
to a signal withObservable
. This subscribes for you and unsubscribes on destroy. ProvidetoSignal(obs$, { initialValue: ... })
to avoid theinitialValue
initial state.undefined - Convert a signal to an Observable with
when you need to compose it with RxJS operators.toObservable(sig) - Prefer signal inputs (
) overinput()
for new components — they integrate with the reactivity graph natively.@Input() - Do not call
or.set()
inside a.update()
— computed signals must be pure.computed() - Wrap mutable signal state in a service when it needs to be shared across components.
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 }]); } }
Details
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)); });
guarantees: toSignal
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.
Source
https://angular.dev/guide/signals
Process
- Read the instructions and examples in this document.
- Apply the patterns to your implementation, adapting to your specific context.
- Verify your implementation against the details and edge cases listed above.
Harness Integration
- Type: knowledge — this skill is a reference document, not a procedural workflow.
- No tools or state — consumed as context by other skills and agents.
Success Criteria
- The patterns described in this document are applied correctly in the implementation.
- Edge cases and anti-patterns listed in this document are avoided.