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.
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()
Recommended Alternatives
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
- YES: Use
-
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:
- Calls
set()orupdate()on signals - Likely state propagation anti-pattern - Calls service methods that update state - Hidden state propagation
- Emits events based on signal changes - Signal/event semantic mismatch
- Has try/catch for async operations - Should use Resource API
- Would benefit from debouncing/throttling - Should use RxJS
Migration Strategy
When converting effects to alternatives:
- Identify effect purpose - State derivation? Async load? Event handling?
- Choose appropriate alternative - Use decision tree above
- Implement replacement - Follow patterns in this skill
- Test thoroughly - Ensure reactive flow works correctly
- Remove effect - Clean up unused code
Key Principles
- Effects are for side effects, not state management
- Prefer declarative over imperative
- Use computed() for sync, Resource API for async
- React to events, not state changes
- RxJS for complex reactive flows
- 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)