Parallel Queries & Mutations
Import
import { queryById, mutationById } from '@ng-query/ngrx-signals-rxjs';
import { rxQueryById, rxMutationById } from '@ng-query/ngrx-signals-rxjs';
Overview
withQueryById
and withMutationById
are specialized versions of withQuery
and withMutation
designed for handling collections of resources identified by a unique key (such as an ID). They work with queryById
/rxQueryById
and mutationById
/rxMutationById
respectively.
Common use cases
Pagination
queryById
can be used to cache multiples pagesmutationById
can be used for parallel granular mutation (target a specific item of a page)
Cache multiples page data
- by using
queryById
instead ofquery
enable to cache all the data made by the same query (eg: A user details page that use the userId from the url to query the user. When usingqueryById
it will saved all the visited user data in cache (in-memory by default)) - In this scenario you should use
mutationById
to target a specific user query and enable parallel mutation
Specific Options queryById
/ rxQueryById
/ mutationById
/ rxMutationById
identifier: (mandatory)
- Property to specify how each resource is grouped (e.g., by
id
). - This enables parallel management of multiple resources that are stored using a Map key/value, where the key is generated by the identifier
- Property to specify how each resource is grouped (e.g., by
equalParams:
Only required when the params are an object.
Under the hood, a resource is generated for each new identifier generated when the params source change.
If the params source change, and their is an existing resource with the same identifier, it will be re-used.
In this case, when the source is an object, an existing resource can be retrieved by the matching his record key with identifier function, but as the reference change it will trigger the loading of the resource again.
To avoid this, you can use this option to tell how to compare the incoming params with the existing params of the resource.
- 'useIdentifier': will use the identifier function to compare the previous params and the incoming params. This very useful when using pagination.
- 'default' (default value): will use a strict equality check (===) between the previous params and the incoming params.
- (a: Params, b: Params, identifierFn: (params: Params) => GroupIdentifier) => boolean: you can provide your own comparison function to compare the previous params and the incoming params. This is useful when you want to compare specific fields of the params.
Note: if your params is a primitive (string, number, boolean, etc.), you don't need to use this option since the strict equality check will work as expected.
For queries the default value is 'useIdentifier'
For mutations the default value is 'default'
params$:
- Only available for queries and resources utilities.
- Accept an observable as the params source
- TODO backlog: can generate all the page/mutations as the methods
method:
- Only available for mutation utilities
- Will be exposed to the store to generate a mutation
store.mutateXXXX(...)
- TODO backlog: can generate all the page/mutations
Resource Options
- For non rx utilities all the resource options, from the official doc
- For rx utilities all the resource options, from the official doc
QueryById & MutationById Effects
When reacting to a mutationById, or trigger an imperative effect to a queryById, the filter option is mandatory:
- filter:
- When reacting to a queryById or mutationById, you should use a
filter
function to determine which resource(s) should be affected by an effect (optimistic update, patch, reload, etc). - The effect is only applied if
filter
returnstrue
for the given identifier.
- When reacting to a queryById or mutationById, you should use a
DANGER
queryById
, rxQueryById
mutationById
and rxMutationById
relies on signal source, only the last value emitted in very short period of time is considered. (A possible evolution is creating a withRxMutationById
associated with rxMutationById
that relies on observables, the same for queries). For more info on this limitation
Usage Example
Store Setup
const Store = signalStore(
withState({ usersFetched: [] as User[], selectedUserId: undefined as string | undefined }),
withMutationById('user', () =>
mutationById({
method: (user: User) => user,
loader: ({ params: user }) => updateUser(user),
identifier: ({ id }) => id,
})
),
withQueryById(
'user',
(store) =>
queryById({
params: store.selectedUserId,
loader: ({ params }) => fetchUser(params),
identifier: (params) => params,
}),
() => ({
on: {
userMutationById: {
optimisticPatch: {
name: ({ mutationParams }) => mutationParams.name,
},
filter: ({ mutationParams, queryIdentifier }) => mutationParams.id === queryIdentifier,
},
},
})
)
);
You can access a specific resource by ID in your component:
// For queryById
const user5 = store.userQueryById()['5'];
if (user5?.hasValue()) {
// Use user5 data
}
// for the mutationById
store.userMutationById()['5']?.hasValue();
// To mutate a specific user
store.mutateUser({ id: '5', name: 'New Name', email: 'new@example.com' });