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.

install
source · Clone the upstream repo
git clone https://github.com/agents-inc/skills
Claude Code · Install into ~/.claude/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"
manifest: src/skills/web-state-ngrx-signalstore/SKILL.md
source content

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:


<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>

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:

  1. Functional Composition - Build stores by composing features like
    withState
    ,
    withComputed
    ,
    withMethods
  2. Signal-Based Reactivity - Leverage Angular's fine-grained reactivity for optimal performance
  3. Immutable Updates - Use
    patchState()
    for predictable state transitions
  4. Extensibility - Create custom features with
    signalStoreFeature()
    for reuse across stores

Key Architecture Decisions:

  1. signalStore()
    creates a fully typed store as an injectable Angular service
  2. patchState()
    ensures immutable updates without Immer dependency
  3. withEntities()
    provides standardized entity management (normalized
    ids
    /
    entityMap
    structure with built-in CRUD updaters)
  4. rxMethod()
    bridges Angular Signals with RxJS for complex async flows
  5. Protected state (v18+) prevents external mutations by default; v19+ applies deep freeze recursively

State Ownership:

State TypeSolutionReason
Server/API dataHTTP services + rxMethodCaching in store, fetch via services
Shared client stateSignalStoreReactivity, composition, DevTools
Component-local stateAngular signalsSimpler, no overhead
URL state (filters)Router query paramsShareable, bookmarkable
</philosophy>
<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:

  1. withState()
    - Define state
  2. withComputed()
    - Derived values from state
  3. withMethods()
    - Actions that update state
  4. 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 requiredYesNo
Operators (debounce, switchMap)YesNo
Race condition handlingBuilt-inManual
Input typesT, 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.


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,
  })),
);
</patterns>
<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
    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.

</integration>

<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>