Files
ISA-Frontend/.claude/skills/ngrx-resource-api/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

8.3 KiB

name, description
name description
ngrx-resource-api This skill should be used when implementing Angular's Resource API with NgRx Signal Store for reactive data management. Use when creating signal stores that load data reactively, need automatic race condition prevention, or require declarative resource management without RxJS. Applies to data-access libraries, feature stores with API integration, and components needing reactive filtering or pagination.

NgRx Resource API

Overview

This skill enables integration of Angular's Resource API with NgRx Signal Store to create reactive data flows without RxJS while automatically preventing race conditions. The Resource API handles concurrent request management declaratively, eliminating manual switchMap or takeUntilDestroyed patterns.

Core Architectural Concepts

Reactive Flow Graph

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 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 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.

When to Use This Pattern

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

Consider alternatives 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)