| name | web-state-ngrx-signalstore |
| description | NgRx SignalStore patterns for Angular state management. Use when managing client state with Angular Signals, composing store features, handling entities, or integrating RxJS effects. Use when this capability is needed. |
| metadata | {"author":"agents-inc"} |
NgRx SignalStore Patterns
Quick Guide: Use NgRx SignalStore for reactive client state in Angular 17+. Compose stores with withState, withComputed, withMethods. Use patchState for immutable updates. Use withEntities for collections. NEVER use traditional NgRx patterns (actions, reducers, effects) in new SignalStore code.
Detailed Resources:
- examples/core.md - signalStore, withState, withComputed, withMethods, withProps
- examples/entities.md - withEntities, CRUD operations, prependEntity/upsertEntity (v20+)
- examples/effects.md - rxMethod, signalMethod (v19+), side effects
- examples/features.md - signalStoreFeature, custom features, DevTools
- examples/testing.md - Unit tests, unprotected(), mocking strategies
- examples/migration.md - Migration from traditional NgRx
- reference.md - Decision frameworks, anti-patterns, red flags
<critical_requirements>
CRITICAL: Before Managing State with NgRx SignalStore
(You MUST use patchState() for ALL state updates - NEVER mutate state directly)
(You MUST wrap async operations in rxMethod() from @ngrx/signals/rxjs-interop for RxJS integration)
(You MUST use withEntities() from @ngrx/signals/entities for entity collections - NOT arrays in state)
(You MUST use named exports ONLY - NO default exports in any store files)
(You MUST use named constants for ALL numbers - NO magic numbers in state code)
</critical_requirements>
Auto-detection: NgRx SignalStore, signalStore, withState, withComputed, withMethods, patchState, withEntities, rxMethod, signalStoreFeature, @ngrx/signals
When to use:
- Managing reactive client state in Angular 17+ applications
- Building composable, reusable store features
- Handling entity collections with CRUD operations
- Integrating RxJS operators for side effects
- Applications requiring fine-grained reactivity with Angular Signals
Key patterns covered:
- Store creation with
signalStore() and providedIn options
- State, computed, and methods composition
- Entity management with
withEntities
- RxJS integration with
rxMethod and signal-only signalMethod
- Custom features with
signalStoreFeature and context-aware withFeature (v20+)
- Derived reactive state with
withLinkedState (v20+)
- Call state patterns (loading, loaded, error)
When NOT to use:
- Server/API data as primary source (use HTTP services with rxMethod for caching)
- Simple component-local state (use Angular signals directly)
- Projects still on Angular < 17 (requires signals)
- Teams unfamiliar with functional composition patterns
Philosophy
NgRx SignalStore is a lightweight, functional state management solution built on Angular Signals. It replaces traditional NgRx patterns (actions, reducers, effects, selectors) with a composable, feature-based approach that eliminates boilerplate while maintaining predictability.
Core Principles:
- Functional Composition - Build stores by composing features like
withState, withComputed, withMethods
- Signal-Based Reactivity - Leverage Angular's fine-grained reactivity for optimal performance
- Immutable Updates - Use
patchState() for predictable state transitions
- Extensibility - Create custom features with
signalStoreFeature() for reuse across stores
Key Architecture Decisions:
signalStore() creates a fully typed store as an injectable Angular service
patchState() ensures immutable updates without Immer dependency
withEntities() provides standardized entity management (normalized ids/entityMap structure with built-in CRUD updaters)
rxMethod() bridges Angular Signals with RxJS for complex async flows
- Protected state (v18+) prevents external mutations by default; v19+ applies deep freeze recursively
State Ownership:
| State Type | Solution | Reason |
|---|
| Server/API data | HTTP services + rxMethod | Caching in store, fetch via services |
| Shared client state | SignalStore | Reactivity, composition, DevTools |
| Component-local state | Angular signals | Simpler, no overhead |
| URL state (filters) | Router query params | Shareable, bookmarkable |
Core Patterns
Pattern 1: Basic Store with signalStore
Create stores using signalStore() with withState, withComputed, and withMethods.
Feature Ordering
Features execute in order. State features must come first:
withState() - Define state
withComputed() - Derived values from state
withMethods() - Actions that update state
withHooks() - Lifecycle hooks (onInit, onDestroy)
For implementation examples, see examples/core.md.
Pattern 2: State Updates with patchState
Use patchState() for all state modifications. It ensures immutability and proper signal notifications.
When to Use
- Synchronous state updates
- Partial state updates (spread not needed)
- Inside
withMethods() and custom features
Key Behaviors
- Accepts partial state object or updater functions
- Multiple updaters can be passed to single call
- Works with entity updaters from
@ngrx/signals/entities
For implementation examples, see examples/core.md.
Pattern 3: Entity Management with withEntities
Use withEntities() for collections of items with IDs. Provides standardized CRUD operations and efficient lookups.
When to Use
- Lists of items with unique identifiers
- CRUD operations on collections
- Need for efficient ID-based lookups
- Multiple entity collections in one store
Available Updaters
setAllEntities() - Replace all entities
addEntity() / addEntities() - Add new entities
setEntity() / setEntities() - Upsert entities
updateEntity() / updateEntities() - Partial updates
removeEntity() / removeEntities() - Delete entities
For implementation examples, see examples/entities.md.
Pattern 4: RxJS Integration with rxMethod
Use rxMethod() for side effects that need RxJS operators (debounce, switchMap, etc.).
When to Use
- Debounced search/filtering
- Cancellable HTTP requests
- Complex async flows with multiple operators
- Reactive streams from signals
Key Behaviors
- Accepts
Observable<T>, Signal<T>, or T as input
- Factory function receives
Observable<T> for piping
- Runs in injection context (can use
inject())
- Auto-unsubscribes on store destroy
For implementation examples, see examples/effects.md.
Pattern 4b: Signal-Only Side Effects with signalMethod (v19+)
Use signalMethod() for side effects that don't need RxJS operators.
When to Use
- Simple side effects without RxJS dependency
- When you want to work purely with Signals
- Side effects that don't require debounce/switchMap/cancellation
- When injection context is not available
Key Behaviors
- Accepts
Signal<T> or T as input (no Observable)
- Processor function runs independently of injection context
- Only parameter signals are tracked; internal signals remain untracked
- You must handle race conditions manually (no built-in switchMap)
rxMethod vs signalMethod
| Feature | rxMethod | signalMethod |
|---|
| RxJS required | Yes | No |
| Operators (debounce, switchMap) | Yes | No |
| Race condition handling | Built-in | Manual |
| Input types | T, Signal, Observable | T, Signal |
For implementation examples, see examples/effects.md.
Pattern 5: Custom Features with signalStoreFeature
Use signalStoreFeature() to create reusable store functionality.
When to Use
- Shared patterns across multiple stores (loading state, error handling)
- Encapsulated feature modules
- Third-party store extensions
- DRY principle for repeated store logic
Type Constraints
Use type() helper to specify required store members:
- Ensures feature is only used with compatible stores
- Provides full type inference
- Enables composition of features
For implementation examples, see examples/features.md.
Pattern 6: Lifecycle Hooks with withHooks
Use withHooks() for initialization and cleanup logic.
Available Hooks
onInit() - Called when store is instantiated
onDestroy() - Called when store is destroyed
Common Use Cases
- Loading initial data on store creation
- Setting up subscriptions
- Cleanup of resources
- DevTools initialization
For implementation examples, see examples/core.md.
Pattern 7: Call State Pattern
Track loading, loaded, and error states for async operations using withCallState() from ngrx-toolkit or custom implementation.
States
loading - Operation in progress
loaded - Operation completed successfully
error - Operation failed with error
Benefits
- Consistent loading UI patterns
- Error handling standardization
- Multiple collection support
- DevTools visibility
For implementation examples, see examples/features.md.
Pattern 8: Context-Aware Features with withFeature (v20+)
Use withFeature() to create features that have access to the current store's methods and properties. Unlike signalStoreFeature(), withFeature receives the store instance, enabling reusable features that can call store-specific methods.
When to Use
- Features that need to call existing store methods
- Generic patterns like entity loaders that need store-specific fetch logic
- When
signalStoreFeature() is insufficient because the feature needs store context
import { withFeature } from "@ngrx/signals";
export const ProductStore = signalStore(
{ providedIn: "root" },
withMethods((store) => ({
load: rxMethod<string>(),
})),
withFeature((store) =>
withEntityLoader((id) => firstValueFrom(store.load(id))),
),
);
Pattern 9: Derived Reactive State with withLinkedState (v20+)
Use withLinkedState() to create state signals that are automatically recomputed when their source signals change. Unlike withComputed(), linked state is writable and can be overridden.
When to Use
- Derived state that also needs to be independently settable
- Maintaining selection state across data changes
- State that has a default derived value but can be overridden by user action
import { withLinkedState } from "@ngrx/signals";
export const FilterStore = signalStore(
withState({ items: [] as Item[] }),
withLinkedState(({ items }) => ({
selectedId: () => items()[0]?.id ?? null,
})),
);
Integration Guide
Angular DI Integration:
SignalStore is an Angular service. Use { providedIn: 'root' } for singletons or component-level providers: [Store] for scoped instances that are destroyed with the component.
See examples/core.md Pattern 5 and Component-Level Stores for DI patterns.
DevTools Integration:
Use withDevtools('name') from @angular-architects/ngrx-toolkit for Redux DevTools support. Place it before withState in the feature chain.
See examples/features.md Pattern 5 for setup.
Testing Integration:
- Use
unprotected() from @ngrx/signals/testing to bypass state protection for test setup
- Use
TestBed.configureTestingModule() with mocked services
- Test store methods directly or through component integration
See examples/testing.md for patterns.
<red_flags>
RED FLAGS
High Priority Issues:
- Mutating state directly instead of using
patchState - breaks reactivity and signal notifications
- Using arrays instead of
withEntities for entity collections - loses normalized state, O(1) lookups
- Not wrapping async RxJS flows in
rxMethod - loses cancellation and proper cleanup
- Accessing store state outside computed/template - may not trigger updates
Medium Priority Issues:
- Missing error handling in
rxMethod pipelines
- Deeply nested state (flatten with multiple state slices)
- Not using
entityConfig() for custom entity IDs (v18+)
- Using
signalMethod for async operations with race conditions (use rxMethod with switchMap)
Gotchas & Edge Cases:
withHooks.onInit() runs during store instantiation - be careful with side effects in tests
patchState with entity updaters requires collection option when using named collections
rxMethod auto-unsubscribes on destroy - do not manually unsubscribe
- Protected state (v18+) prevents external
patchState calls by default
- Feature execution order matters - later features can access earlier ones
- v19+ Deep Freeze: State values are recursively frozen with
Object.freeze in dev mode - use withProps() for mutable objects like FormGroup
See reference.md for detailed anti-patterns with code examples.
</red_flags>
<critical_reminders>
CRITICAL REMINDERS
(You MUST use patchState() for ALL state updates - NEVER mutate state directly)
(You MUST wrap async operations in rxMethod() from @ngrx/signals/rxjs-interop for RxJS integration)
(You MUST use withEntities() from @ngrx/signals/entities for entity collections - NOT arrays in state)
(You MUST use named exports ONLY - NO default exports in any store files)
(You MUST use named constants for ALL numbers - NO magic numbers in state code)
Failure to follow these rules will cause reactivity bugs, state corruption, and inconsistent behavior.
</critical_reminders>
Converted and distributed by TomeVault — claim your Tome and manage your conversions.