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.
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:
- Filter signals trigger resource loading - Parameter changes automatically reload resources
- Methods explicitly invoke operations - Use
signalMethodfor user-triggered actions - 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
paramssignal changes abortSignalenables 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 booleanhasError()- Error state booleanerror()- Error object if presentstatus()- 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
abortSignalto 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:
- Idle - Initial state before first load
- Loading - Request in progress
- Resolved - Data loaded successfully
- 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
rxMethodor 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
rxMethodinstead)