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.
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()
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
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:
- 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 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 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 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
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.
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
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
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)
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)