Files
ISA-Frontend/.claude/skills/state-patterns/SKILL.md
Lorenz Hilpert 7612394ba1 ♻️ refactor(claude): reorganize and rename skills for clarity
Consolidate and rename skills to shorter, more intuitive names:
- css-keyframes-animations → css-animations
- api-sync-manager → api-sync
- architecture-documentation → arch-docs
- jest-vitest-patterns → test-migration
- reactive-state-patterns → state-patterns
- library-scaffolder → library-creator

Merge related skills:
- angular-template + html-template → template-standards
- angular-effects-alternatives + ngrx-resource-api → state-patterns

Remove obsolete skills:
- architecture-enforcer (merged into architecture-validator)
- circular-dependency-resolver (merged into architecture-validator)
- standalone-component-migrator (merged into migration-specialist agent)
- swagger-sync-manager (replaced by api-sync)
- api-change-analyzer (merged into api-sync)
- type-safety-engineer (content distributed to relevant skills)
- test-migration-specialist (replaced by migration-specialist agent)

Add migration-specialist agent for standalone and test migrations.
Update all cross-references in CLAUDE.md and agents.
2025-12-11 11:30:05 +01:00

18 KiB

name, description
name description
state-patterns This skill should be used when writing Angular code with signals, effects, and NgRx Signal Store. Use when deciding whether to use effect(), computed(), or reactive patterns for state management. Applies when implementing Resource API with Signal Store for reactive data loading, preventing race conditions, and creating declarative async flows. Essential for code review of effect usage, refactoring imperative patterns to declarative alternatives, and building stores with reactive filter/search parameters.

Reactive State Patterns

Overview

This skill guides proper usage of Angular's effect(), provides declarative alternatives for common patterns, and enables integration of Angular's Resource API with NgRx Signal Store. Effects are frequently misused for state propagation, leading to circular updates and maintenance issues. The Resource API provides race-condition-free async data loading that integrates seamlessly with Signal Store.

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.

Effect Anti-Patterns (NEVER Use)

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

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

Resource API with NgRx Signal Store

Core Architectural Concepts

Establish three clear interaction points in the store:

  1. Filter signals trigger resource loading - Parameter changes automatically reload resources
  2. Methods explicitly invoke operations - Use signalMethod for user-triggered actions
  3. Computed signals derive view models - Transform loaded data for component consumption

The withProps Pattern for Dependency Injection

Inject services via withProps with underscore-prefixed properties to mark them as internal implementation details:

withProps(() => ({
  _dataService: inject(DataService),
  _notificationService: inject(ToastService),
}))

Benefits:

  • Centralizes dependency injection in one location
  • Clear distinction between internal (prefixed) and public properties
  • Services available to all subsequent feature sections

Implementation Steps

Step 1: Inject Services with withProps

Create the initial withProps section to inject required services:

export const MyStore = signalStore(
  withProps(() => ({
    _dataService: inject(DataService),
    _toastService: inject(ToastService),
  })),
  // ... additional features
);

Naming convention: Prefix all injected services with underscore (_) to indicate internal use.

Step 2: Define Filter State

Add state properties that will serve as resource parameters:

withState({
  filter: {
    searchTerm: '',
    category: '',
  } as MyFilter,
})

Step 3: Configure Resources

In a subsequent withProps section, create resources that reference the injected services and state:

withProps((store) => ({
  _itemsResource: resource({
    params: store.filter,
    loader: (loaderParams) => {
      const filter = loaderParams.params;
      const abortSignal = loaderParams.abortSignal;
      return store._dataService.loadItems(filter, abortSignal);
    }
  })
}))

Key points:

  • Resources automatically reload when params signal changes
  • abortSignal enables automatic cancellation of in-flight requests
  • Loader must return a Promise (use .findPromise() if service returns Observable)

Step 4: Expose Read-Only Resources (Optional)

If the resource should be accessible to consumers, expose it as read-only:

withProps((store) => ({
  itemsResource: store._itemsResource.asReadonly(),
}))

Pattern: Internal resources use underscore prefix, public versions are read-only without prefix.

Step 5: Create Signal Methods for Updates

Use signalMethod for actions that update state and trigger resource reloads:

withMethods((store) => ({
  updateFilter: signalMethod<MyFilter>((filter) => {
    patchState(store, { filter });
  }),

  refresh: () => {
    store._itemsResource.reload();
  }
}))

Important: signalMethod implementations are untracked by convention - they don't re-execute when signals change. This provides explicit control flow.

Step 6: Add Error Handling

Use withHooks to react to resource errors:

withHooks({
  onInit: (store) => {
    effect(() => {
      const error = store._itemsResource.error();
      if (error) {
        store._toastService.show('Error: ' + getMessage(error));
      }
    });
  }
})

Pattern: Error effects should be read-only - they observe errors and trigger side effects, but don't modify state.

Common Resource API Patterns

Template Integration with linkedSignal

For two-way form binding that synchronizes with the store:

export class MyComponent {
  #store = inject(MyStore);

  // Create linked signal for form field
  searchTerm = linkedSignal(() => this.#store.filter().searchTerm);

  // Combine form fields into filter object
  #linkedFilter = computed(() => ({
    searchTerm: this.searchTerm(),
    // ... other fields
  }));

  constructor() {
    // Sync form changes back to store
    this.#store.updateFilter(this.#linkedFilter);
  }
}

Benefits:

  • Two-way binding for forms
  • Automatic store synchronization
  • Type-safe filter construction

Working with Resource Data

Access resource data through resource signals:

withComputed((store) => ({
  items: computed(() => store._itemsResource.value() ?? []),
  isLoading: computed(() => store._itemsResource.isLoading()),
  hasError: computed(() => store._itemsResource.hasError()),
}))

Available signals:

  • value() - The loaded data (undefined while loading)
  • isLoading() - Loading state boolean
  • hasError() - Error state boolean
  • error() - Error object if present
  • status() - Overall status: 'idle' | 'loading' | 'resolved' | 'error'

Updating Resource Data Locally

For temporary working copies before server writes:

withMethods((store) => ({
  updateLocalItem: (id: string, changes: Partial<Item>) => {
    store._itemsResource.update((currentItems) => {
      return currentItems.map(item =>
        item.id === id ? { ...item, ...changes } : item
      );
    });
  }
}))

Note: This pattern feels unconventional but aligns with maintaining temporary working copies before server persistence.

Multiple Resources in One Store

Combine multiple resources for complex data requirements:

withProps((store) => ({
  _itemsResource: resource({
    params: store.filter,
    loader: (params) => store._dataService.loadItems(params.params, params.abortSignal)
  }),

  _detailsResource: resource({
    params: store.selectedId,
    loader: (params) => {
      if (!params.params) return Promise.resolve(null);
      return store._dataService.loadDetails(params.params, params.abortSignal);
    }
  })
}))

Pattern: Each resource has independent params and loading state, but can share service instances.

Important Resource API Considerations

Race Condition Prevention

The Resource API automatically handles race conditions:

  • New requests automatically cancel pending requests
  • No need for switchMap, takeUntilDestroyed, or manual abort handling
  • Declarative parameter changes trigger clean cancellation

Untracked Signal Methods

signalMethod implementations deliberately skip reactive tracking:

  • Provides explicit, predictable control flow
  • Prevents unexpected re-executions from signal changes
  • Makes side effects obvious at call sites

Loader Function Requirements

Loaders must:

  • Return a Promise (not Observable)
  • Accept and pass through the abortSignal to enable cancellation
  • Handle the signal in the underlying HTTP call

Converting Observables:

loader: (params) => {
  return firstValueFrom(
    this._service.load$(params.params)
      .pipe(takeUntilDestroyed(this._destroyRef))
  );
}

Resource Lifecycle

Resources maintain their own state machine:

  1. Idle - Initial state before first load
  2. Loading - Request in progress
  3. Resolved - Data loaded successfully
  4. Error - Request failed

State transitions automatically trigger reactive updates to dependent computeds and effects.

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

When to Use Each Pattern

Use computed() when:

  • Synchronous state derivation
  • No async operations needed
  • Memoization desired

Use Resource API with Signal Store when:

  • Loading data based on reactive filter/search parameters
  • Need automatic race condition handling for concurrent requests
  • Want declarative data loading without RxJS subscriptions
  • Building stores with frequently changing query parameters
  • Implementing pagination, filtering, or search features

Use RxJS integration when:

  • Complex reactive flows with multiple operators
  • Need debouncing, throttling, or custom timing control
  • Working with multiple Observable sources
  • Require fine-grained control over subscription lifecycle

Use reactive helpers when:

  • Prefer signal-centric API but need RxJS power
  • Simple async operations with basic operators
  • Want automatic subscription management

Consider alternatives to Resource API when:

  • Simple one-time data loads (use rxMethod or direct service calls)
  • Complex Observable chains with multiple operators needed
  • Need fine-grained control over request timing/caching
  • Working with WebSocket or SSE streams (use rxMethod instead)

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)