@ng-query in Signal Store - Overview
If the Angular resource
are not enough for you, you may appreciate this tool that provide utilities function to handle server state management in declarative and reactive way.
INFO
This tool evolves continuously based on community feedback and needs. The initial version required overcoming significant TypeScript challenges to integrate properly with signalStore
. Having addressed these foundational typing complexities, this tool is now well-positioned for future enhancements and feature additions.
Why You May Need This Tool
Signal Store for Server State simplifies complex data operations with features like:
- Declarative optimistic updates for responsive UIs
- Seamless integration between client and server state
- Centralized management of loading, error, and success states
- Easy to use with CRUD or BFF (backend for frontend)
- React easily to mutation
- Use
Insertions
for better comfort based on reusable composition pattern
Why Signal Store for Server State?
Integrating server state management directly within signalStore
provides a seamless approach to handling queries and mutations. This integration offers a declarative and reactive pattern that simplifies complex server interactions.
For more advanced scenarios, you can associate client state with query state, leveraging the full power of Signal Store for a unified state management solution that handles both client and server concerns elegantly.
Server state management tool - philosophy
It is not a client state management tool, the idea:
- The query state represent the resource in your backend
- In this way, it should only be updated by using mutation reactions, reloads, or by the cache manager (called
persister
) - But, you can derived a client state by using
withLinkedSignal
or update an existing state from your store (see Queries section) - Client state should be local as possible (only share client state globally if you really need it), otherwise prefer to expose globally server state.
- Make the queries and mutations declarative (it abstracts a lot of imperative code and avoid some boilerplate)
From my experience, most of client state tools suggests indirectly to work with the fetched state. In my opinion, it is a bad practice. If a server state is used in multiples component, a change that may not be commit (saved in the backend) will result by displaying a wrong data to the user.
Current Implementation Details
This implementation is built on Angular's signals
using a state-driven approach. While this offers a fully synchronous solution with predictable behavior, it comes with certain limitations compared to Observable-based patterns (event driven approach). For more information about the differences between pull-based signals and push-based observables, see this detailed article.
RxJs is optional, but may be required for handling more advanced case (for retry strategy, interval...)
Quick start overview : Handle server state management inside the signalStore
import { signalStore, withQuery } from '@ngrx/signals';
import { query } from './query';
const Store = signalStore(
withState({
user: undefined as User | undefined,
userSelected: undefined as { id: string } | undefined,
}),
withMutation(
'userEmail',
// 👇 access to the store if needed
(store) =>
mutation({
// 👇 expose a method: store.mutateUserEmail({ id: '5', email: 'mutated@test.com', });
method: ({ id, email }: { id: string; email: string }) => ({
id,
email,
}),
loader: ({ params }) => store._api.updateEmail(params),
})
),
withQuery(
'user',
// 👇 access to the store
(store) =>
query({
params: store.userSelected,
loader: ({ params: { id } }) => store._api.getUser(id),
}),
// 👇 access to the store if needed
(store) => ({
associatedClientState: {
user: true, // will update the state.user to with the fetchUser data
},
on: {
userEmailMutation: {
// 👇 Perform optimistic update each time the mutation is loading
optimisticUpdate: ({ queryResource, mutationParams }) => {
return {
...queryResource.value(),
email: mutationParams.email,
};
},
// 👇 Perform optimistic patch each time the mutation is loading
optimisticPatch: {
email: ({ mutationParams }) => mutationParams?.email,
},
reload: {
onMutationError: true, //👈 Reload the query if the mutation failed
},
},
},
})
)
);
// Inject the store and use the query resource
const store = inject(Store);
// store.userQuery (expose the `ResourceRef API`)
const user = store.userQuery.value(); // Access the fetched user
const status = store.userQuery.status(); // 'idle', 'loading', 'resolved', 'error'
// trigger a mutation
store.mutateUserEmail({ id: '5', email: 'mutated@test.com' });