web-state-ngrx-signalstore
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:
- 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()andprovidedInoptions - State, computed, and methods composition
- Entity management with
withEntities - RxJS integration with
rxMethodand signal-onlysignalMethod - Custom features with
signalStoreFeatureand context-awarewithFeature(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 servicepatchState()ensures immutable updates without Immer dependencywithEntities()provides standardized entity management (normalizedids/entityMapstructure 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 statewithComputed()- Derived values from statewithMethods()- Actions that update statewithHooks()- 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 entitiesaddEntity()/addEntities()- Add new entitiessetEntity()/setEntities()- Upsert entitiesupdateEntity()/updateEntities()- Partial updatesremoveEntity()/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, 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 instantiatedonDestroy()- 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 progressloaded- Operation completed successfullyerror- 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";
// withFeature receives the store instance
export const ProductStore = signalStore(
{ providedIn: "root" },
withMethods((store) => ({
load: rxMethod<string>(/* fetch logic */),
})),
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 }) => ({
// Recomputes when items change, but can also be set independently
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/testingto 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
withEntitiesfor 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
rxMethodpipelines - Deeply nested state (flatten with multiple state slices)
- Not using
entityConfig()for custom entity IDs (v18+) - Using
signalMethodfor async operations with race conditions (userxMethodwithswitchMap)
Gotchas & Edge Cases:
withHooks.onInit()runs during store instantiation - be careful with side effects in testspatchStatewith entity updaters requirescollectionoption when using named collectionsrxMethodauto-unsubscribes on destroy - do not manually unsubscribe- Protected state (v18+) prevents external
patchStatecalls by default - Feature execution order matters - later features can access earlier ones
- v19+ Deep Freeze: State values are recursively frozen with
Object.freezein dev mode - usewithProps()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>
More from agents-inc/skills
web-animation-css-animations
CSS Animation patterns - transitions, keyframes, scroll-driven animations, @property, GPU-accelerated properties, accessibility with prefers-reduced-motion
20web-testing-playwright-e2e
Playwright E2E testing patterns - test structure, Page Object Model, locator strategies, assertions, network mocking, visual regression, parallel execution, fixtures, and configuration
18web-animation-framer-motion
Motion (formerly Framer Motion) animation patterns - motion components, variants, gestures, layout animations, scroll-linked animations, accessibility
17web-animation-view-transitions
View Transitions API patterns - same-document transitions, cross-document MPA transitions, shared element animations, pseudo-element styling, accessibility
16web-styling-cva
Class Variance Authority - type-safe component variant styling with cva(), compound variants, and VariantProps
16web-performance-web-performance
Bundle optimization, render performance, Core Web Vitals
16