Files
ISA-Frontend/.claude/skills/angular-effects-alternatives/SKILL.md
Lorenz Hilpert f3d5466f81 feat: add NgRx Resource API and Angular effects alternatives skills
Add two new skills based on Angular Architects articles:

1. ngrx-resource-api: Guide for integrating Angular's Resource API with
   NgRx Signal Store for reactive data management without RxJS
   - withProps pattern for dependency injection
   - Resource configuration and lifecycle
   - Error handling and computed derivations
   - Common patterns and best practices

2. angular-effects-alternatives: Guide for proper effect() usage and
   declarative alternatives to prevent anti-patterns
   - Valid use cases (logging, canvas, imperative APIs)
   - Anti-patterns to avoid (state propagation, synchronization)
   - Decision tree for choosing alternatives
   - Refactoring patterns and code review checklist

Both skills follow modern Angular patterns and promote declarative,
maintainable code aligned with reactive principles.
2025-11-20 15:46:50 +01:00

11 KiB

name, description
name description
angular-effects-alternatives This skill should be used when writing Angular code with signals and effects. Use when deciding whether to use effect(), computed(), or reactive patterns for state management. Applies to all Angular components and services using signals, especially when considering effect() for state propagation, data synchronization, or reactive flows. Essential for code review of effect usage and refactoring imperative patterns to declarative alternatives.

Angular Effects Alternatives

Overview

This skill guides proper usage of Angular's effect() and provides declarative alternatives for common patterns. Effects are frequently misused for state propagation, leading to circular updates, maintenance issues, and violations of reactive principles. This skill prevents anti-patterns and promotes maintainable, declarative code.

When to Use Effects (Valid Use Cases)

Effects are primarily for rendering content that cannot be rendered through data binding. Valid use cases are limited to:

1. Logging

Recording application events or debugging:

effect(() => {
  const error = this.error();
  if (error) {
    console.error('Error occurred:', error);
  }
});

2. Canvas Painting

Custom graphics rendering (e.g., Angular Three library, Chart.js integration):

effect(() => {
  const data = this.chartData();
  this.renderCanvas(data);
});

3. Custom DOM Behavior

Imperative APIs that require direct DOM manipulation:

effect(() => {
  const message = this.notificationMessage();
  if (message) {
    this.snackBar.open(message, 'Close');
  }
});

Key principle: Data binding is the preferred way to display data. Effects should only be used when data binding is insufficient.

Understanding Auto-Tracking

Angular automatically tracks signals accessed during effect execution, even within called methods:

effect(() => {
  this.logError(); // Signal tracking happens inside this method
});

logError(): void {
  const error = this.error(); // This signal is automatically tracked
  if (error) {
    console.error(error);
  }
}

Implication: Auto-tracking makes effect dependencies non-obvious and hard to maintain. This is a primary reason to avoid effects for state management.

When NOT to Use Effects (Anti-Patterns)

Anti-Pattern 1: State Propagation

NEVER use effects to propagate state changes to other state:

// ❌ WRONG - Anti-pattern
effect(() => {
  const value = this.source();
  this.derived.set(value * 2);
});

Problems:

  • Risk of circular updates and infinite loops
  • Hard to maintain due to implicit tracking
  • Violates declarative reactive principles
  • Inappropriate glitch-free semantics

Anti-Pattern 2: Synchronizing Signals

// ❌ WRONG - Anti-pattern
effect(() => {
  const filter = this.filter();
  this.loadData(filter);
});

Anti-Pattern 3: Event Emulation

// ❌ WRONG - Anti-pattern
effect(() => {
  const count = this.itemCount();
  this.countChanged.emit(count);
});

Why signals ≠ events: Signals are designed to be glitch-free, collapsing multiple updates. This makes them inappropriate for representing discrete events.

Decision Tree: Effect vs Alternative

Need to react to signal changes?
│
├─ Synchronous derivation?
│  └─ ✅ Use computed()
│
├─ Asynchronous derivation?
│  └─ ✅ Use Resource API
│
├─ Complex reactive flow with race conditions?
│  └─ ✅ Use RxJS (toObservable + operators + toSignal)
│
├─ Event-based trigger (not state change)?
│  └─ ✅ React to event directly, not signal
│
├─ Need RxJS operators with signals?
│  └─ ✅ Use reactive helpers (rxMethod, deriveAsync)
│
└─ Rendering non-data-bound content (logging, canvas, imperative API)?
   └─ ✅ Use effect()

Alternative 1: Use computed() for Synchronous Derivations

When to use: Deriving new state synchronously from existing signals.

// ✅ CORRECT - Declarative
const derived = computed(() => {
  return this.baseSignal() * 2;
});

const fullName = computed(() => {
  return `${this.firstName()} ${this.lastName()}`;
});

Benefits:

  • Declarative and maintainable
  • Automatic dependency tracking
  • Memoized and efficient
  • No risk of circular updates

Alternative 2: Use Resource API for Asynchronous Derivations

When to use: Loading data based on reactive parameters.

// ✅ CORRECT - Declarative async state
readonly itemsResource = resource({
  params: this.filter,
  loader: ({ params, abortSignal }) => {
    return this.dataService.load(params, abortSignal);
  }
});

readonly items = computed(() => this.itemsResource.value() ?? []);

Benefits:

  • Automatic race condition handling
  • Built-in loading/error states
  • Declarative parameter tracking
  • Cancellation support

See also: ngrx-resource-api skill for detailed Resource API patterns.

Alternative 3: React to Events, Not State Changes

When to use: User interactions or DOM events should trigger actions.

// ❌ WRONG - Reacting to signal change
effect(() => {
  const searchTerm = this.searchTerm();
  this.search(searchTerm);
});

// ✅ CORRECT - React to event
<input (input)="search($event.target.value)" />

// Component
search(term: string): void {
  this.searchTerm.set(term);
  this.performSearch(term);
}

Benefits:

  • Clear causality (event → action)
  • No auto-tracking complexity
  • Explicit control flow

Alternative 4: RxJS Integration

When to use: Complex reactive flows requiring operators like debounceTime, switchMap, combineLatest.

// ✅ CORRECT - RxJS for complex flows
readonly searchTerm = signal('');
readonly searchTerm$ = toObservable(this.searchTerm);

readonly results$ = this.searchTerm$.pipe(
  debounceTime(300),
  distinctUntilChanged(),
  switchMap(term => this.searchService.search(term))
);

readonly results = toSignal(this.results$, { initialValue: [] });

Benefits:

  • Full RxJS operator ecosystem
  • Race condition prevention (switchMap)
  • Powerful composition
  • Type-safe streams

Pattern: Signal → Observable (toObservable) → RxJS operators → Signal (toSignal)

Alternative 5: Reactive Helpers

When to use: Need RxJS operators but prefer signal-centric API.

Using rxMethod (NgRx Signal Store)

readonly loadItem = rxMethod<number>(
  pipe(
    tap(() => patchState(this, { loading: true })),
    switchMap(id => this.service.findById(id)),
    tap(item => patchState(this, { item, loading: false }))
  )
);

// Call with signal or value
constructor() {
  this.loadItem(this.selectedId);
}

Using deriveAsync (ngxtension)

readonly data = deriveAsync(() => {
  const filter = this.filter();
  return this.service.load(filter);
});

Benefits:

  • Signal-friendly API
  • RxJS operator support
  • Cleaner than manual Observable conversion
  • Automatic subscription management

The explicitEffect Consideration

Some libraries provide explicitEffect to restrict auto-tracking:

// Uses untracked() internally to limit tracking
explicitEffect(this.id, (id) => {
  this.store.load(id);
});

Evaluation:

  • Mitigates auto-tracking drawbacks
  • Makes dependencies explicit
  • Still imperative (not declarative)
  • Doesn't solve circular update risks
  • Less idiomatic than reactive alternatives

Recommendation: Prefer declarative patterns (computed, Resource API, RxJS) over explicitEffect.

Common Refactoring Patterns

Pattern 1: Effect for State Sync → computed()

// ❌ Before
effect(() => {
  const x = this.x();
  const y = this.y();
  this.sum.set(x + y);
});

// ✅ After
readonly sum = computed(() => this.x() + this.y());

Pattern 2: Effect for Async Load → Resource API

// ❌ Before
effect(() => {
  const id = this.selectedId();
  this.loadItem(id);
});

// ✅ After
readonly itemResource = resource({
  params: this.selectedId,
  loader: ({ params }) => this.service.loadItem(params)
});

Pattern 3: Effect for Debounced Search → RxJS

// ❌ Before
effect(() => {
  const term = this.searchTerm();
  // No way to debounce within effect
  this.search(term);
});

// ✅ After
readonly searchTerm$ = toObservable(this.searchTerm);
readonly results = toSignal(
  this.searchTerm$.pipe(
    debounceTime(300),
    switchMap(term => this.searchService.search(term))
  ),
  { initialValue: [] }
);

Pattern 4: Effect for Event Notification → Direct Event Handling

// ❌ Before
effect(() => {
  const value = this.value();
  this.valueChange.emit(value);
});

// ✅ After
updateValue(newValue: string): void {
  this.value.set(newValue);
  this.valueChange.emit(newValue);
}

Code Review Checklist

When reviewing code with effect(), ask:

  • Is this for rendering non-data-bound content? (logging, canvas, imperative APIs)

    • YES: Effect is appropriate
    • NO: Continue checklist
  • Is this synchronous state derivation?

    • YES: Use computed() instead
  • Is this asynchronous data loading?

    • YES: Use Resource API instead
  • Does this need RxJS operators (debounce, switchMap, etc.)?

    • YES: Use RxJS integration or reactive helpers instead
  • Is this reacting to a user event?

    • YES: Handle event directly instead
  • Could this cause circular updates?

    • YES: Refactor immediately - this will cause bugs

Anti-Pattern Detection Rules

Flag any effect that:

  1. Calls set() or update() on signals - Likely state propagation anti-pattern
  2. Calls service methods that update state - Hidden state propagation
  3. Emits events based on signal changes - Signal/event semantic mismatch
  4. Has try/catch for async operations - Should use Resource API
  5. Would benefit from debouncing/throttling - Should use RxJS

Migration Strategy

When converting effects to alternatives:

  1. Identify effect purpose - State derivation? Async load? Event handling?
  2. Choose appropriate alternative - Use decision tree above
  3. Implement replacement - Follow patterns in this skill
  4. Test thoroughly - Ensure reactive flow works correctly
  5. Remove effect - Clean up unused code

Key Principles

  1. Effects are for side effects, not state management
  2. Prefer declarative over imperative
  3. Use computed() for sync, Resource API for async
  4. React to events, not state changes
  5. RxJS for complex reactive flows
  6. Auto-tracking is powerful but opaque - avoid when possible

When in Doubt

Ask: "Can the user see this without an effect using data binding?"

  • YES: Don't use effect, use data binding
  • NO: Effect might be appropriate (but verify against decision tree)