NgRx Toolkit

repository·main·Indexed 43 days ago

https://github.com/angular-architects/ngrx-toolkit

A collection of extensions for the NgRx Signal Store providing advanced patterns such as Redux integration, Redux Devtools support, and web storage synchronization. It includes the Redux Connector via createReduxState() to map NgRx actions to store methods, as well as support for resources, mutations, and immutable state protection. Available as @ngrx-toolkit/core for version 22 and later.

Tokens
31.8K
Snippets
94
Records
112
Agent score
55%

What's inside NgRx Toolkit

  1. Overview of NgRx Toolkit features

    main

    NgRx Toolkit provides a set of extensions for the NgRx Signals Store to enhance its capabilities. Key features include:

    • Devtools: Integration with Redux Devtools for state inspection.
    • Redux: Support for the Redux pattern (Actions, Reducers, and Effects) within the Signals Store context.
    • Storage Sync: Automatic synchronization of the Store with Web Storage (e.g., localStorage).
    • Redux Connector: A mechanism to map existing NgRx Store Actions to a present Signal Store using createReduxState().
  2. Available NgRx Toolkit extensions

    main

    The NgRx Toolkit provides a variety of extensions for the NgRx SignalStore. These extensions allow you to add specialized functionality like devtools integration, data synchronization, and state protection.

    Key extensions include:

    • Devtools: Integration with Redux Devtools.
    • Conditional Features: Add features to the store based on conditions.
    • DataService: Extends withEntities with backend synchronization.
    • Immutable State Protection: Prevents state mutation inside or outside the Store.
    • Resource: Integrates Angular's Resource API into SignalStore for async operations.
    • Entity Resources: Extends withResource with entity support (ids, entityMap, entities).
    • Mutations: Provides an equivalent to signal resources for sending data back to the backend.
    • Reset: Adds a resetState method to the store.
    • Call State: Adds call state management.
    • Storage Sync: Synchronizes the Store with Web Storage.
    • Undo Redo: Adds Undo/Redo functionality.

    Note: The withRedux extension is deprecated in favor of NgRx's @ngrx/signals/events (available starting in NgRx 19.2).

  3. What is the Redux Connector for NgRx Signal Store?

    main

    The Redux Connector turns any signalStore() into a Global State Management Slice that follows the Redux pattern. It allows you to bridge the gap between the NgRx Signal Store and the global NgRx Store by mapping actions to store methods.

    It is available via the secondary entry point: import { createReduxState } from '@ngrx-toolkit/core/redux-connector'.

    Note: This package has a dependency on @ngrx/store.

  4. Use withResource() to connect Angular Resource API to NgRx SignalStore

    main

    The withResource() extension allows an NgRx SignalStore to directly manage asynchronous data by wiring in Angular's Resource API (e.g., httpResource). It provides two distinct modes of operation: Single Resource and Named Resources.

    Single Resource Mode

    Use this when the store is dedicated to a single data source. The store itself implements the Resource<T> interface, exposing properties directly on the store instance:

    • value(): The current resource value.
    • status(): The current resource status.
    • error(): Any error encountered.
    • isLoading(): Boolean indicating if the resource is loading.
    • hasValue(): A type guard to check if a value exists.
    • _reload(): A method to trigger a reload.

    Named Resources Mode

    Use this when a store manages multiple entities or async operations. Each resource is assigned a name, and all its properties are prefixed with that name:

    • [name]Value()
    • [name]Status()
    • [name]Error()
    • [name]IsLoading()
    • [name]HasValue()
    • _[name]Reload()
    import { withResource } from '@ngrx-toolkit/core';
    import { signalStore, withState } from '@ngrx/signals';
    import { httpResource } from '@angular/core';
    
    // Single Resource Example
    export const UserStore = signalStore(
      withState({ userId: 1 }),
      withResource((state) => httpResource(() => `/user/${state.userId}`)),
    );
    
    // Multiple Named Resources Example
    export const UserStore = signalStore(
      withState({ userId: undefined as number | undefined }),
      withResource(({ userId }) => ({
        list: httpResource<User[]>(() => '/users', { defaultValue: [] }),
        detail: httpResource<User>(() => (userId === undefined ? undefined : `/user/${userId}`)),
      })),
    );
  5. Use withEntityResources() to integrate array resources with NgRx Entities

    main

    The withEntityResources() function integrates Angular Resources that return arrays into an NgRx SignalStore using the Entity helpers from @ngrx/signals/entities. It provides an entity view (like ids, entityMap, and entities) over the data returned by the resource.

    Key Concepts

    • Source of Truth: The underlying Angular Resource remains the source of truth. Entity state is linked to the resource value using linked signals.
    • Mutations: You can use NgRx entity updaters (like addEntity, updateEntity, removeEntity) to mutate the entity view in the store. These mutations are synchronized with the resource via signals without using effects.
    • Requirements:
      • Each resource must resolve to an array (use defaultValue: [] for consistency).
      • Array elements must include an id compatible with EntityId.

    If your resource does not produce an array, use withResource() instead.

    import { withEntityResources } from '@ngrx-toolkit/core';
    
    // Single (unnamed) resource
    withEntityResources<Entity extends { id: EntityId }>((store) => ResourceRef<readonly Entity[] | Entity[] | undefined>);
    
    // Multiple (named) resources
    withEntityResources<Dictionary extends Record<string, ResourceRef<readonly unknown[] | unknown[] | undefined>>>((store) => Dictionary);
  6. Compare rxMutation and httpMutation

    main

    Choose the mutation type based on your data source:

    • rxMutation: Use this when your operation is an RxJS stream (e.g., calling a service method that returns an Observable). You provide an operation function that takes parameters and returns an Observable.
    • httpMutation: Use this for direct HttpClient calls. It is RxJS-agnostic and can be configured in two ways:
      • Function style: Pass a function that receives parameters and returns an HTTP request configuration object.
      • Options style: Pass an object containing a request function (to build the config) and an optional parse function to transform the response.
    // rxMutation example
    rxMutation({
      operation: (params: Params) => {
        return calcSum(this.counterSignal(), params.value);
      },
    })
    
    // httpMutation (Function style)
    httpMutation((userData: CreateUserRequest) => ({
      url: '/api/users',
      method: 'POST',
      body: userData,
    }))
    
    // httpMutation (Options style)
    httpMutation({
      request: (p: Params) => ({
        url: `https://httpbin.org/post`,
        method: 'POST',
        body: { counter: p.value },
      }),
      parse: (res) => res as CounterResponse,
    });
  7. Choose between `rxMutation` and `httpMutation`

    main

    The toolkit provides two primary mutation types depending on your data source:

    1. rxMutation: Use this when you want to utilize RxJS streams. The operation property is a function that returns an Observable. This is suitable for any logic involving RxJS, even if it isn't HTTP-related.

    2. httpMutation: Use this for making HttpClient requests. It is designed to feel like the HttpClient API. You can pass parameters using the standard HttpClient method signature or a request object. It includes a parse option to transform the response type.

    FeaturerxMutationhttpMutation
    Core Logicoperation: (params) => Observable<T>request: (params) => HttpRequest or method signature
    Primary Use CaseGeneral RxJS logicHTTP requests
  8. Use Mutations for async write operations

    main

    The NgRx Toolkit provides rxMutation and httpMutation to handle asynchronous write operations (like POST, PUT, DELETE) that are not suitable for Angular's resource API. While resource is intended for read operations (GET), mutations are designed for imperative requests that change state.

    Mutations can be used in two ways:

    1. Inside a SignalStore: Using the withMutations feature.
    2. Standalone: As class members in components or services.
    import { httpMutation, rxMutation, withMutations } from '@ngrx-toolkit/core';
    
    // Inside a SignalStore
    withMutations((store) => ({
      increment: rxMutation({
        operation: (params: Params) => {
          return calcSum(store.counter(), params.value);
        },
      }),
      saveToServer: httpMutation({
        request: (_: void) => ({
          url: `https://httpbin.org/post`,
          method: 'POST',
          body: { counter: store.counter() },
        }),
        parse: (response) => response as CounterResponse,
      }),
    }))
  9. Understand Mutation state signals

    main

    Every mutation provides a set of reactive signals to track its lifecycle and data. These signals allow you to reactively update your UI based on the mutation's progress.

    If using withMutations within a signalStore, access signals via the store using the pattern store.mutationNameSignal (e.g., store.incrementStatus). If using mutation functions standalone (e.g., as class members), access them directly via the mutation instance (e.g., mutation.status).

    Available Signals:

    • status: A signal of type 'idle' | 'pending' | 'error' | 'success'.
    • value: A signal containing the Result of the mutation (or undefined).
    • isPending: A boolean signal indicating if the mutation is currently running.
    • isSuccess: A boolean signal indicating if the last mutation completed successfully.
    • error: A signal containing the error object if the mutation failed.
    • hasValue(): A type-guard method that narrows the type of the .value() signal to exclude undefined if the mutation was successful.
    // Types for reference
    export type MutationStatus = 'idle' | 'pending' | 'error' | 'success';
    
    export type Mutation<Parameter, Result> = {
      status: Signal<'idle' | 'pending' | 'error' | 'success'>;
      value: Signal<Result | undefined>;
      isPending: Signal<boolean>;
      isSuccess: Signal<boolean>;
      error: Signal<unknown>;
      hasValue(): this is Mutation<Exclude<Parameter, undefined>, Result>;
    };
  10. Configure dynamic property names to avoid conflicts

    main

    By default, withDataService and its companion features use generic property names (e.g., entities, loading, filter). To avoid naming collisions in complex stores, you can provide a collection name in the configuration objects of withCallState, withEntities, withDataService, and withUndoRedo.

    When a collection name is provided (e.g., 'flight'), the generated signals and methods will be prefixed with that name (e.g., flightEntities, flightLoading, updateFlightFilter). This configuration is fully type-safe.

    import { withDataService, withCallState, withUndoRedo } from '@ngrx-toolkit/core';
    
    export const FlightBookingStore = signalStore(
      { providedIn: 'root' },
      withCallState({
        collection: 'flight',
      }),
      withEntities({
        entity: type<Flight>(),
        collection: 'flight',
      }),
      withDataService({
        dataServiceType: FlightService,
        filter: { from: 'Graz', to: 'Hamburg' },
        collection: 'flight',
      }),
      withUndoRedo({
        collections: ['flight'],
      }),
    );
  11. Find community content and tutorials for NgRx Toolkit

    main

    There is a variety of community-driven content available to help you master the NgRx Toolkit, including:

    • Blog Posts: Search for tutorials on combining withResource with NgRx Signal Stores.
    • Video Content: Look for YouTube shorts on topics like 'Local Storage Sync with Ngrx Signal Store' or livestreams from 'Angular Space' and 'Jason Warner' discussing withResource and withMutations.
    • Discord: Join the NgRx Toolkit Discord to share content or engage in general discussions that don't require a formal GitHub issue.
  12. Restore a passing lint baseline after migration

    main

    ESLint v9 and typescript-eslint v8 may enable new rules by default that were previously inactive. The goal is to reach a state where nx run-many -t lint passes without modifying source code.

    Handling Rule Violations:

    1. Distinguish between findings and crashes:
      • Plugin Crash: If you see a TypeError (e.g., context.getAncestors is not a function) or a 'could not find config' error, the plugin is incompatible with ESLint v9. Do not disable the rule. Instead, update the plugin to a version that supports v9.
      • Rule Finding: If the rule reports a standard lint error, proceed to step 2.
    2. Disable new preset rules: If a rule is reporting errors but was NOT explicitly configured by the user in the previous version, disable it in the eslint.config.mjs with a comment explaining why.
    3. Preserve user rules: Never disable or weaken a rule that the user had explicitly configured in the previous version.

    Constraint: If you cannot reach a passing state without editing source files or disabling a user-configured rule, the migration has failed and must be reported.

    nx run-many -t lint
    // Disable a rule that a changed preset default newly enabled
    export default [
      ...baseConfig,
      {
        files: ['**/*.ts'],
        rules: {
          // Newly enabled by the ESLint v9 recommended set; was not enforced before the upgrade.
          'no-unused-expressions': 'off',
        },
      },
    ];