NgRx Traits

website·Indexed 30 days ago

https://ngrx-traits.dev

A library for NgRx that provides store features for backend calls, entity management (filtering, pagination, selection, and sorting), and state synchronization with URLs, Web Storage, and SSR. It includes a variety of traits such as withEntities, withCalls, and withSyncToRouteQueryParams, as well as utility functions and a writeTo mechanism.

Tokens
40K
Snippets
184
Records
214
Agent score
49%

What's inside NgRx Traits

  1. Overview of NgRx Traits for NgRx Signals

    NgRx Traits is a collection of custom store features designed for NgRx Signals. It accelerates development by providing pre-built solutions for common state management tasks, including backend API integration, pagination, sorting, filtering, and entity selection.
  2. Overview of Ngrx Traits

    Ngrx Traits provides a set of prebuilt NgRx signals Custom Store Features to solve common state management problems. It reduces boilerplate by generating strongly typed signals and methods for tasks such as loading entities, managing backend operation statuses, filtering, sorting, pagination (including infinite scroll), entity selection, and synchronizing state with web storage or route parameters.
  3. Overview of Ngrx Traits Store Features

    Ngrx Traits provides a set of reusable store features (traits) that can be plugged into a signalStore to implement common state management patterns. Key feature categories include:

    • Backend Calls: Managing loading states and results (e.g., withCalls, withCallStatus).
    • Entity Management: Filtering, pagination, selection, and sorting of entity collections (e.g., withEntitiesLocalPagination, withEntitiesRemoteFilter).
    • State Synchronization: Syncing state to URL query parameters, Web Storage, or SSR (e.g., withSyncToWebStorage, withRouteParams).
    • Utilities: Logging, feature factories, and state setters.
  4. Manage parallel call statuses with withCallStatusMap

    The withCallStatusMap trait generates the state, computed signals, and methods necessary to track the progress status of backend calls. Unlike a single status trait, it uses a map (keyed by string or number), allowing multiple calls of the same type to run in parallel, each with its own independent status. This is particularly useful for scenarios not handled by withEntitiesCalls.
    import { withCallStatusMap } from '@ngrx-traits/signals';
    
    const store = signalStore(withCallStatusMap({ prop: 'loadDetails' }));
  5. Manage multiple entity collections with custom query param prefixes

    When syncing multiple entity collections within a single store, use the prefix option in withEntitiesSyncToRouteQueryParams to prevent query parameter collisions.

    Because signalStore has a limit of 15 features, it is recommended to group each collection's traits into a custom signalStoreFeature using entityConfig to maintain readability and avoid hitting the feature limit.

    // Define config for a collection
    export const productEntityConfig = entityConfig({
      entity: type<Product>(),
      collection: 'product',
    });
    
    export function withProductEntities() {
      return signalStoreFeature(
        withEntities(productEntityConfig),
        withCallStatus({ ...productEntityConfig, initialValue: 'loading' }),
        // ... other traits
        withEntitiesSyncToRouteQueryParams({
          ...productEntityConfig,
          prefix: 'p', // Params become p-filter, p-page, etc.
        }),
      );
    }
    
    // Compose multiple collection features into one store
    export const ProductsShopStore = signalStore(
      withProductEntities(),
      withOrderEntities(),
    );
  6. Ensure type inference between dependent signalStoreFeatures

    To enable full type inference and autocompletion in factory functions when one feature depends on another, follow these patterns:

    1. Wrap your signalStoreFeature in a function.
    2. Use the type() helper as the first argument to the signalStoreFeature call, passing the type of the dependent feature (e.g., type<ProductEntitiesStoreFeature>()).
    3. This allows the extracted type to include all state, props, and methods from the combined features.
  7. Implement two-way sync with mismatched types using readFrom and writeTo

    When an external model() type does not match the store's state type, syncWith cannot be used. Instead, combine readFrom (to map the model into the store's type) and writeTo (to map store changes back to the model). The store's equal guard prevents echo loops between these two mappings.
    @Component({ /* ... */ })
    export class ProductSearchComponent {
      store = inject(ProductsStore);
      // Parent uses string, store uses { search: string }
      search = model<string>('');
    
      linked = this.store.linkProductEntitiesFilter({
        // Map model -> store type
        readFrom: computed(() => ({ search: this.search() })),
        // Map store type -> model
        writeTo: (value) => this.search.set(value.search),
      });
    }
  8. Trigger remote data fetching for hybrid filters

    When withEntitiesHybridFilter determines a remote filter is required (via isRemoteFilter), it sets the collection to a loading state. To actually fetch the data from a backend, you should either create a custom effect that listens to the loading state or use the withEntitiesLoadingCall trait to map the filter parameters to an API call.
    withEntitiesLoadingCall({
      ...entityConfig,
      fetchEntities: ({ productEntitiesFilter }) => {
        return inject(ProductService).getProducts({
          categoryId: productEntitiesFilter().categoryId,
        });
      },
    }),
  9. Implement multi-selection for entities using withEntitiesMultiSelection

    The withEntitiesMultiSelection trait generates the state, computed signals, and methods required to handle the selection of multiple entities. It requires withEntities to be present in the store.

    Caveat: isAll[Collection]Selected and toggleSelectAll[Collection] will not work correctly when using remote pagination, as they cannot access the full dataset on the server.

    import { signalStore } from '@ngrx-traits/signals';
    import { withEntities, withEntitiesMultiSelection, entityConfig } from '@ngrx-traits/signals';
    
    const entity = entityConfig({
        entity: type<Product>(),
        collection: 'product'
    });
    
    const store = signalStore(
        withEntities(entity),
        withEntitiesMultiSelection(entity)
    );
    
  10. Create private state setters with withStatePrivateSetter

    The withStatePrivateSetter trait generates private setter methods for specified state properties. For each provided state prop, it creates a method named _set[PropName](). Because the method name starts with an underscore, @ngrx/signals excludes it from the store's public type, making it callable only from within the store (e.g., inside withMethods). This is used when a property should be publicly readable but only writable internally.

    Generated setters are signalMethods and support:

    1. Plain values.
    2. Signals or reactive functions (to keep state in sync).
    3. Updater functions (current) => next for partial updates.
    import { signalStore, withState, withMethods } from '@ngrx/signals';
    import { withStatePrivateSetter } from '@ngrx-traits/signals';
    
    const Store = signalStore(
      withState({
        filter: '',
        d: 12,
      }),
      // generates _setFilter()
      withStatePrivateSetter('filter'),
      withMethods((store) => ({
        clearFilter: () => store._setFilter(''), // usable inside the store
      })),
    );
    
    const store = new Store();
    store.filter(); // readable from outside
    store.clearFilter();
    // store._setFilter('x'); // compile error: not part of the public store type
  11. Log signal store state changes with withLogger

    The withLogger trait from @ngrx-traits/signals logs state signals and computed signals of the store to the console on every change. It can be configured with a name for identification and an optional filter to limit which signals are logged.
    import { signalStore, withState, withEntities } from '@ngrx-traits/signals';
    import { withLogger } from '@ngrx-traits/signals';
    
    const store = signalStore(
      withState({ foo: 'foo' }),
      withEntities({entity: type<Product>()}),
      withLogger({ name: "MyStore" }),
    );
  12. Synchronize multi-selected entity IDs using withLinkEntitiesMultiSelection

    withLinkEntitiesMultiSelection is a prebuilt version of withLink specifically for withEntitiesMultiSelection. It generates a link[Collection]IdsSelected() method that connects the store's selected entity IDs to Angular component signals such as input(), model(), or Signal Forms.

    Key behaviors:

    • It reads from the [collection]IdsSelected computed signal.
    • It writes to the store via select[Collection]Entities with clearSelectionBeforeSelect enabled (meaning each write replaces the current selection, and an empty array clears it).
    • It uses order-insensitive ID equality to prevent echo loops, as the selection map does not preserve ID order.
    • Requirement: withEntitiesMultiSelection must be used in the store before withLinkEntitiesMultiSelection.
    const entity = type<Genre>();
    
    export const GenresStore = signalStore(
      { providedIn: 'root' },
      withEntities({ entity }),
      withEntitiesMultiSelection({ entity }),
      // generates linkIdsSelected()
      withLinkEntitiesMultiSelection({ entity }),
    );