Skills web-state-ngrx-signalstore
NgRx SignalStore patterns for Angular state management. Use when managing client state with Angular Signals, composing store features, handling entities, or integrating RxJS effects.
git clone https://github.com/agents-inc/skills
T=$(mktemp -d) && git clone --depth=1 https://github.com/agents-inc/skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/src/skills/web-state-ngrx-signalstore" ~/.claude/skills/agents-inc-skills-web-state-ngrx-signalstore-1a8b19 && rm -rf "$T"
src/skills/web-state-ngrx-signalstore/SKILL.mdNgRx SignalStore Patterns
Quick Guide: Use NgRx SignalStore for reactive client state in Angular 17+. Compose stores with
,withState,withComputed. UsewithMethodsfor immutable updates. UsepatchStatefor collections. NEVER use traditional NgRx patterns (actions, reducers, effects) in new SignalStore code.withEntities
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
for ALL state updates - NEVER mutate state directly)patchState()
(You MUST wrap async operations in
from rxMethod()
for RxJS integration)@ngrx/signals/rxjs-interop
(You MUST use
from withEntities()
for entity collections - NOT arrays in state)@ngrx/signals/entities
(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
andsignalStore()
optionsprovidedIn - State, computed, and methods composition
- Entity management with
withEntities - RxJS integration with
and signal-onlyrxMethodsignalMethod - Custom features with
and context-awaresignalStoreFeature
(v20+)withFeature - Derived reactive state with
(v20+)withLinkedState - 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>
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
,withComputedwithMethods - Signal-Based Reactivity - Leverage Angular's fine-grained reactivity for optimal performance
- Immutable Updates - Use
for predictable state transitionspatchState() - Extensibility - Create custom features with
for reuse across storessignalStoreFeature()
Key Architecture Decisions:
creates a fully typed store as an injectable Angular servicesignalStore()
ensures immutable updates without Immer dependencypatchState()
provides standardized entity management (normalizedwithEntities()
/ids
structure with built-in CRUD updaters)entityMap
bridges Angular Signals with RxJS for complex async flowsrxMethod()- 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:
- Define statewithState()
- Derived values from statewithComputed()
- Actions that update statewithMethods()
- Lifecycle hooks (onInit, onDestroy)withHooks()
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
and custom featureswithMethods()
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
- Replace all entitiessetAllEntities()
/addEntity()
- Add new entitiesaddEntities()
/setEntity()
- Upsert entitiessetEntities()
/updateEntity()
- Partial updatesupdateEntities()
/removeEntity()
- Delete entitiesremoveEntities()
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>
, orSignal<T>
as inputT - Factory function receives
for pipingObservable<T> - 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
orSignal<T>
as input (no Observable)T - 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 | | |
|---|---|---|
| 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
- Called when store is instantiatedonInit()
- Called when store is destroyedonDestroy()
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
- Operation in progressloading
- Operation completed successfullyloaded
- Operation failed with errorerror
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
is insufficient because the feature needs store contextsignalStoreFeature()
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
</patterns>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>
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
fromunprotected()
to bypass state protection for test setup@ngrx/signals/testing - Use
with mocked servicesTestBed.configureTestingModule() - Test store methods directly or through component integration
See examples/testing.md for patterns.
</integration><red_flags>
RED FLAGS
High Priority Issues:
- Mutating state directly instead of using
- breaks reactivity and signal notificationspatchState - Using arrays instead of
for entity collections - loses normalized state, O(1) lookupswithEntities - Not wrapping async RxJS flows in
- loses cancellation and proper cleanuprxMethod - Accessing store state outside computed/template - may not trigger updates
Medium Priority Issues:
- Missing error handling in
pipelinesrxMethod - Deeply nested state (flatten with multiple state slices)
- Not using
for custom entity IDs (v18+)entityConfig() - Using
for async operations with race conditions (usesignalMethod
withrxMethod
)switchMap
Gotchas & Edge Cases:
runs during store instantiation - be careful with side effects in testswithHooks.onInit()
with entity updaters requirespatchState
option when using named collectionscollection
auto-unsubscribes on destroy - do not manually unsubscriberxMethod- Protected state (v18+) prevents external
calls by defaultpatchState - Feature execution order matters - later features can access earlier ones
- v19+ Deep Freeze: State values are recursively frozen with
in dev mode - useObject.freeze
for mutable objects like FormGroupwithProps()
See reference.md for detailed anti-patterns with code examples.
</red_flags>
<critical_reminders>
CRITICAL REMINDERS
(You MUST use
for ALL state updates - NEVER mutate state directly)patchState()
(You MUST wrap async operations in
from rxMethod()
for RxJS integration)@ngrx/signals/rxjs-interop
(You MUST use
from withEntities()
for entity collections - NOT arrays in state)@ngrx/signals/entities
(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>