Getting Started
This library provides a variety of store features for working with entities. Here, you'll learn how they work, how to combine them, and best practices for using them.
There are multiple ways to load entities, ranging from a more manual approach to a fully automated one. Below, we'll explore these methods step by step.
withCallStatus
You should only need this approach if you are dealing with complex loading logic that cannot be easily handled by the withCalls
and withEntitiesLoading
store features.
With withCallStatus
, we manage the loading state manually by using status-related signals such as isLoading
, isLoaded
, and error
, along with their corresponding setters: setLoading
, setLoaded
, and setError
added byy withCallStatus
. In this example we will use a withHook
but they could be use in a withMethod as well.
const productEntityConfig = entityConfig({
entity: type<Product>(),
collection: 'products',
});
export const ProductsRemoteStore = signalStore(
{ providedIn: 'root' },
withEntities(productEntityConfig),
withCallStatus({ ...productEntityConfig, initialValue: 'loading' }),
// 👆 adds signals isProductsLoading(), isProductsLoaded(), productsError()
// and methods setProductsLoading() setProductsLoaded(), setProductsError(error)
withMethods(({setProductsLoading, setProductsLoaded, setProductsError, ...store}) => ({
loadProducts: rxMethod(pipe(switchMap(() => {
setProductsLoading()
return inject(ProductService)
.getProducts()
.pipe(
tap((res) =>
patchState(
store,
setAllEntities(res.resultList, { collection: 'products' }),
),
),
catchError((error) => {
setProductsError(error);
return EMPTY;
}));
})))
}))
);
withCalls
Using withCalls
eliminates the need to manually update the call status, reducing boilerplate code. However, it is not specifically designed to work with withEntities
, so you still need to manually store the entities in the state.
In contrast, when using withEntitiesLoadingCall
, this process is handled automatically.
const productsEntityConfig = entityConfig({
entity: type<Product>(),
collection: 'products',
});
const store = signalStore(
withEntities(productsEntityConfig),
withCalls((store) => ({
loadProducts: callConfig({
call: () => inject(ProductService).getProducts(),
storeResult: false,
onSuccess: (res) => {
patchState(
store,
setAllEntities(res.resultList, productsEntityConfig),
);
},
}),
})),
);
withCalls
in a Store with withEntitiesLocalFilter
and withEntitiesLocalSort
This approach is similar to the previous one. When using local filtering and sorting, you must manually trigger sorting and filtering after loading the entities.
This example is included mainly for completeness. However, in most cases, you should use withEntitiesLoadingCall
instead, as it automatically handles sorting and filtering if they are present.
const productsEntityConfig = entityConfig({
entity: type<Product>(),
collection: 'products',
});
export const Store = signalStore(
{ providedIn: 'root' },
withEntities({ entity, collection }),
withEntitiesLocalFilter({
entity,
collection,
defaultFilter: { search: '' },
filterFn: (entity, filter) =>
!filter?.search ||
entity?.name.toLowerCase().includes(filter?.search.toLowerCase()),
}),
withEntitiesLocalSort({
entity,
collection,
defaultSort: { field: 'name', direction: 'asc' },
}),
withCalls((store) => ({
loadProducts: callConfig({
call: () => inject(ProductService).getProducts(),
storeResult: false,
onSuccess: (res) => {
patchState(store, setAllEntities(res.resultList, productsEntityConfig));
// force resort and refilter
store.sortProductsEntities();
store.filterProductsEntities({
filter: store.productsFilter(),
debounce: 0,
forceLoad: true,
});
},
}),
})),
);
withEntitiesLoadingCall
withEntitiesLoadingCall
is specifically designed for loading entities and integrating seamlessly with other withEntities
store features included in this library.
Let's first look at an example, followed by an explanation of how it works.
const productsEntityConfig = entityConfig({
entity: type<Product>(),
collection: 'products',
});
export const ProductsLocalStore = signalStore(
{ providedIn: 'root' },
withEntities(productsEntityConfig),
withCallStatus({ ...productsEntityConfig, initialValue: 'loading' }),
withEntitiesLoadingCall({
...productsEntityConfig,
fetchEntities: () => {
return inject(ProductService)
.getProducts()
.pipe(map((d) => d.resultList));
},
}));
withEntitiesLoadingCall
is reactive—it listens to the isProductsLoading()
signal (or isLoading()
if no collection prop is defined in the entityConfig
). This signal is added by withCallStatus
, and when it is true
, fetchEntities
is called.
Since in our previous example withCallStatus
sets the initialValue
to 'loading'
, the entities will be fetched immediately when the store initializes.
withEntitiesLoadingCall
depend on the loading status from withCallStatus
instead of generating its own?Unlike withCalls
, which manages its own loading state, withEntitiesLoadingCall
relies on withCallStatus
because callStatus
acts as a communication API between all other withEntities*
store features.
For example:
withEntitiesRemoteFilter
calls setProductsLoading()
(or setLoading()
if no collection) when filterEntities
is executed, ensuring fetchEntities
is triggered with the updated filter parameters.withEntitiesRemotePagination
calls setProductsLoading()
when more pages need to be loaded.withEntitiesRemoteSort
does the same when sorting is required.In contrast, withEntitiesLocalFilter
and withEntitiesLocalSort
do not trigger a new fetch. Instead, they reapply filtering and sorting every time isProductsLoaded
is true
.
Below, you can see a few examples illustrating these interactions.
withEntitiesLoadingCall
and OthersBelow is an example of how to use withEntitiesLocalPagination
, withEntitiesLocalFilter
, and withEntitiesLocalSort
alongside withEntitiesLoadingCall
. This setup loads entities from the backend, after which they can be filtered, sorted, and paginated locally.
Notice that in fetchEntities
, you only need to return either an array of entities or an object of type { entities: Entity[], total: number }
. The store will handle storing the entities for you. Additionally, local filtering, sorting, and pagination will automatically reapply whenever the data is reloaded.
const productsEntityConfig = entityConfig({
entity: type<Product>(),
collection: 'products',
});
export const ProductsLocalStore = signalStore(
{ providedIn: 'root' },
withEntities(productsEntityConfig),
withCallStatus({ ...productsEntityConfig, initialValue: 'loading' }),
// withCallStatus must be before all the other
// withEntities* features so the can use the callStatus state
withEntitiesLocalPagination({
...productsEntityConfig,
pageSize: 5,
}),
withEntitiesLocalFilter({
...productsEntityConfig,
defaultFilter: { search: '' },
filterFn: (entity, filter) =>
!filter?.search ||
entity?.name.toLowerCase().includes(filter?.search.toLowerCase()),
}),
withEntitiesLocalSort({
...productsEntityConfig,
defaultSort: { field: 'name', direction: 'asc' },
}),
// with withEntitiesLoadingCall, should generally be the after all the other
// withEntities* so it can use the state generated by them,
// you can see this in the next example for remote filtering, sorting pagination
withEntitiesLoadingCall({
...productsEntityConfig,
fetchEntities: () => {
return inject(ProductService)
.getProducts()
.pipe(map((d) => d.resultList));
},
}),
);
withEntitiesLoadingCall
and OthersIn this scenario, sorting, filtering, and pagination are handled by the backend.
Notice that in the fetchEntities
function, we pass the filter, sort, and pagination parameters to the backend, which we read from the signals added by the other withEntities* store features. The backend processes these parameters and returns the filtered, sorted, and paginated entities along with the total count. The store features then takes care of the rest.
const productsStoreFeature = signalStoreFeature(
withEntities({
entity: productsEntity,
collection: productsCollection,
}),
withCallStatus({
initialValue: 'loading',
collection: productsCollection,
errorType: type<string>(),
}),
withEntitiesRemoteFilter({
entity: productsEntity,
collection: productsCollection,
defaultFilter: { search: '' },
}),
withEntitiesRemotePagination({
entity: productsEntity,
collection: productsCollection,
pageSize: 10,
}),
withEntitiesRemoteSort({
entity: productsEntity,
collection: productsCollection,
defaultSort: { field: 'name', direction: 'asc' },
}),
withEntitiesLoadingCall(
({ productsPagedRequest, productsFilter, productsSort }) => ({
collection: productsCollection,
fetchEntities: async () => {
const res = await lastValueFrom(
inject(ProductService).getProducts({
search: productsFilter().search,
skip: productsPagedRequest().startIndex,
take: productsPagedRequest().size,
sortAscending: productsSort().direction === 'asc',
sortColumn: productsSort().field,
}),
);
return { entities: res.resultList, total: res.total };
},
}),
),
);