NgRx Traits
website·Indexed 30 days ago
https://ngrx-traits.devA 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.
What's inside NgRx Traits
- 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.
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.Overview of Ngrx Traits Store Features
Ngrx Traits provides a set of reusable store features (traits) that can be plugged into a
signalStoreto 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.
- Backend Calls: Managing loading states and results (e.g.,
Manage parallel call statuses with withCallStatusMap
ThewithCallStatusMaptrait 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 bywithEntitiesCalls.import { withCallStatusMap } from '@ngrx-traits/signals'; const store = signalStore(withCallStatusMap({ prop: 'loadDetails' }));Manage multiple entity collections with custom query param prefixes
When syncing multiple entity collections within a single store, use the
prefixoption inwithEntitiesSyncToRouteQueryParamsto prevent query parameter collisions.Because
signalStorehas a limit of 15 features, it is recommended to group each collection's traits into a customsignalStoreFeatureusingentityConfigto 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(), );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:
- Wrap your
signalStoreFeaturein a function. - Use the
type()helper as the first argument to thesignalStoreFeaturecall, passing the type of the dependent feature (e.g.,type<ProductEntitiesStoreFeature>()). - This allows the extracted type to include all state, props, and methods from the combined features.
- Wrap your
Implement two-way sync with mismatched types using readFrom and writeTo
When an externalmodel()type does not match the store's state type,syncWithcannot be used. Instead, combinereadFrom(to map the model into the store's type) andwriteTo(to map store changes back to the model). The store'sequalguard 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), }); }Trigger remote data fetching for hybrid filters
WhenwithEntitiesHybridFilterdetermines a remote filter is required (viaisRemoteFilter), 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 thewithEntitiesLoadingCalltrait to map the filter parameters to an API call.withEntitiesLoadingCall({ ...entityConfig, fetchEntities: ({ productEntitiesFilter }) => { return inject(ProductService).getProducts({ categoryId: productEntitiesFilter().categoryId, }); }, }),Implement multi-selection for entities using withEntitiesMultiSelection
The
withEntitiesMultiSelectiontrait generates the state, computed signals, and methods required to handle the selection of multiple entities. It requireswithEntitiesto be present in the store.Caveat:
isAll[Collection]SelectedandtoggleSelectAll[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) );Create private state setters with withStatePrivateSetter
The
withStatePrivateSettertrait 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/signalsexcludes it from the store's public type, making it callable only from within the store (e.g., insidewithMethods). This is used when a property should be publicly readable but only writable internally.Generated setters are
signalMethods and support:- Plain values.
- Signals or reactive functions (to keep state in sync).
- Updater functions
(current) => nextfor 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 typeLog signal store state changes with withLogger
ThewithLoggertrait from@ngrx-traits/signalslogs 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" }), );Synchronize multi-selected entity IDs using withLinkEntitiesMultiSelection
withLinkEntitiesMultiSelectionis a prebuilt version ofwithLinkspecifically forwithEntitiesMultiSelection. It generates alink[Collection]IdsSelected()method that connects the store's selected entity IDs to Angular component signals such asinput(),model(), or Signal Forms.Key behaviors:
- It reads from the
[collection]IdsSelectedcomputed signal. - It writes to the store via
select[Collection]EntitieswithclearSelectionBeforeSelectenabled (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:
withEntitiesMultiSelectionmust be used in the store beforewithLinkEntitiesMultiSelection.
const entity = type<Genre>(); export const GenresStore = signalStore( { providedIn: 'root' }, withEntities({ entity }), withEntitiesMultiSelection({ entity }), // generates linkIdsSelected() withLinkEntitiesMultiSelection({ entity }), );- It reads from the