NgRx SignalStore Patterns
Quick Guide: Use NgRx SignalStore for reactive client state in Angular 17+. Compose stores with
withState,withComputed,withMethods. UsepatchStatefor immutable updates. UsewithEntitiesfor collections. NEVER use traditional NgRx patterns (actions, reducers, effects) in new SignalStore code.
Detailed Resources:
- •For code examples, see examples/ folder:
- •core.md - signalStore, withState, withComputed, withMethods
- •entities.md - withEntities, CRUD operations
- •effects.md - rxMethod, side effects
- •features.md - signalStoreFeature, custom features
- •testing.md - Unit tests, mocking strategies
- •migration.md - Migration from traditional NgRx
- •For decision frameworks and anti-patterns, see reference.md
<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()andprovidedInoptions - •State, computed, and methods composition
- •Entity management with
withEntities - •RxJS integration with
rxMethodand signal-onlysignalMethod - •Custom features with
signalStoreFeature - •Call state patterns (loading, loaded, error)
- •DevTools integration with ngrx-toolkit
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>
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 (similar to Redux Toolkit'screateEntityAdapter) - •
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 |
<patterns>
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>, orTas 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>orTas 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<T>, Observable<T> | T, Signal<T> |
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.
</patterns><integration>
Integration Guide
Angular DI Integration:
SignalStore is an Angular service. Use providedIn: 'root' for singletons or component-level providers for scoped stores:
// Root-level singleton
export const GlobalStore = signalStore(
{ providedIn: "root" },
withState({
/* ... */
}),
);
// Component-scoped
@Component({
providers: [LocalStore],
})
export class MyComponent {
readonly store = inject(LocalStore);
}
DevTools Integration:
Use @angular-architects/ngrx-toolkit for Redux DevTools support:
import { withDevtools } from "@angular-architects/ngrx-toolkit";
export const FlightStore = signalStore(
{ providedIn: "root" },
withDevtools("flights"),
withState({ flights: [] as Flight[] }),
);
Testing Integration:
- •Use
unprotected()from@ngrx/signals/testingto bypass state protection - •Use
TestBed.configureTestingModule()with mocked services - •Test store methods directly or through component integration
<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>