mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-31 09:37:15 +01:00
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.
This commit is contained in:
417
.claude/skills/angular-effects-alternatives/SKILL.md
Normal file
417
.claude/skills/angular-effects-alternatives/SKILL.md
Normal file
@@ -0,0 +1,417 @@
|
||||
---
|
||||
name: angular-effects-alternatives
|
||||
description: 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:
|
||||
|
||||
```typescript
|
||||
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):
|
||||
|
||||
```typescript
|
||||
effect(() => {
|
||||
const data = this.chartData();
|
||||
this.renderCanvas(data);
|
||||
});
|
||||
```
|
||||
|
||||
### 3. Custom DOM Behavior
|
||||
Imperative APIs that require direct DOM manipulation:
|
||||
|
||||
```typescript
|
||||
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**:
|
||||
|
||||
```typescript
|
||||
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:**
|
||||
|
||||
```typescript
|
||||
// ❌ 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
|
||||
|
||||
```typescript
|
||||
// ❌ WRONG - Anti-pattern
|
||||
effect(() => {
|
||||
const filter = this.filter();
|
||||
this.loadData(filter);
|
||||
});
|
||||
```
|
||||
|
||||
### ❌ Anti-Pattern 3: Event Emulation
|
||||
|
||||
```typescript
|
||||
// ❌ 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()
|
||||
```
|
||||
|
||||
## Recommended Alternatives
|
||||
|
||||
### Alternative 1: Use `computed()` for Synchronous Derivations
|
||||
|
||||
**When to use:** Deriving new state synchronously from existing signals.
|
||||
|
||||
```typescript
|
||||
// ✅ 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.
|
||||
|
||||
```typescript
|
||||
// ✅ 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.
|
||||
|
||||
```typescript
|
||||
// ❌ 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`.
|
||||
|
||||
```typescript
|
||||
// ✅ 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)
|
||||
|
||||
```typescript
|
||||
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)
|
||||
|
||||
```typescript
|
||||
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:
|
||||
|
||||
```typescript
|
||||
// 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()
|
||||
|
||||
```typescript
|
||||
// ❌ 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
|
||||
|
||||
```typescript
|
||||
// ❌ 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
|
||||
|
||||
```typescript
|
||||
// ❌ 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
|
||||
|
||||
```typescript
|
||||
// ❌ 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)
|
||||
287
.claude/skills/ngrx-resource-api/SKILL.md
Normal file
287
.claude/skills/ngrx-resource-api/SKILL.md
Normal file
@@ -0,0 +1,287 @@
|
||||
---
|
||||
name: ngrx-resource-api
|
||||
description: 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:
|
||||
|
||||
```typescript
|
||||
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:
|
||||
|
||||
```typescript
|
||||
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:
|
||||
|
||||
```typescript
|
||||
withState({
|
||||
filter: {
|
||||
searchTerm: '',
|
||||
category: '',
|
||||
} as MyFilter,
|
||||
})
|
||||
```
|
||||
|
||||
### Step 3: Configure Resources
|
||||
|
||||
In a subsequent `withProps` section, create resources that reference the injected services and state:
|
||||
|
||||
```typescript
|
||||
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:
|
||||
|
||||
```typescript
|
||||
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:
|
||||
|
||||
```typescript
|
||||
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:
|
||||
|
||||
```typescript
|
||||
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:
|
||||
|
||||
```typescript
|
||||
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:
|
||||
|
||||
```typescript
|
||||
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:
|
||||
|
||||
```typescript
|
||||
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:
|
||||
|
||||
```typescript
|
||||
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:**
|
||||
```typescript
|
||||
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)
|
||||
Reference in New Issue
Block a user