Global Queries
Global queries provide a way to define, cache, and reuse query logic across multiple signal stores and components. The goal is to centralize query definitions, enable shared caching, and simplify integration of common data sources throughout your application.
INFO
This api may be changed (check the last part for more info)
Aim
- Centralization: Define queries in one place, making them easy to maintain and update.
- Reusability: Plug queries into any signal store or inject them directly into components, reducing duplication.
- Caching: Share cached data between stores and components, improving performance and consistency.
- Extensibility: Support for custom persisters, cache time configuration, and dependency injection (e.g., services).
- Avoid error: Defining all globalQueries in the same place avoid to recreate an already existing query and ensure that the key is not already used
- Organization: Define queries at a feature level for better modularization and maintainability.
- Clarity: Make dependencies between features explicit rather than creating a monolithic query library.
Usage Overview
Define global queries: Use
globalQueries
to declare queries. Each query can be configured with cache options and injected dependencies.typescriptexport const { withUserQuery, withUsersQuery, withUserQueryById, injectUserQuery, injectUsersQuery, injectUserQueryById } = globalQueries({ queries: { user: { query: (source: SignalProxy<{ id: string | undefined }>) => rxQuery({ ... }), }, users: { query: () => rxQuery({ ... }), }, }, queriesById: { user: { queryById: () => rxQueryById({ ... }), }, }, });
Plug queries into signal stores: Use the generated
withUserQuery
,withUsersQuery
, orwithUserQueryById
functions to add queries to your signal stores.typescriptconst store = signalStore( withState({ selected: '1' }), withUserQuery((store) => ({ setQuerySource: (source) => ({ id: store.selected }) })), withUsersQuery(), withUserQueryById() );
Inject queries directly: Use the generated
injectUserQuery
orinjectUserQueryById
functions to access query resources in components or services.typescriptprivate readonly userQueryResource = injectUserQuery();
React to mutation All the default options of the queries are usable with global queries. It is possible to react to a mutation, or a mutation to mutate a query state.
signalStore(
{ providedIn: 'root' },
withState({ selected: '1' }),
withMutation('name', () =>
rxMutation({
method: (name: string) => name,
stream: ({ params }) => of({ id: '4', name: params }),
})
),
withUserQuery((store) => ({
on: {
nameMutation: {...},
},
}))
);
Features
Plugging data from component or signalStore
For queries that need dynamic parameters (e.g: an input from a component or a state from a store), define a source SignalProxy
and use the setQuerySource
option when plugging into a signal store, or in the inject function:
globalQueries({
queries: {
user: {
query: (source: SignalProxy<{ id: string | undefined }>) =>
rxQuery({
params: source.id,
stream: ({ params: id }) => api.getUserDetails(id),
}),
},
},
});
const store = signalStore(withUserQuery((store) => ({ setQuerySource: (source) => ({ id: store.selected }) })));
In a component, you can use the pluggable API:
@Component(...) class UserComponent {
// The Angular router will automatically bind userId
// as `withComponentInputBinding` is added to `provideRouter`.
// See https://angular.dev/api/router/withComponentInputBinding
readonly userId = input<string>();
readonly userQueryResource = injectUserQuery((source) => ({ id: this.userId }));
}
Injecting a service
You can inject Angular services directly into your query definitions:
globalQueries({
queries: {
user: {
query: (api = inject(ApiService)) =>
rxQuery({
params: () => "1",
stream: ({ params: id }) => api.getUserDetails(id),
}),
},
users: {
query: (source: SignalProxy<{ id: string | undefined }>, api = inject(ApiService)) =>
rxQuery(...),
}
},
});
This allows queries to use any injectable dependency, such as HTTP clients or custom services.
Modifying cacheTime
You can set the cache duration for each query or globally:
- Per-query:typescript
globalQueries({ queries: { user: { config: { cacheTime: 60000 }, // 1 minute query: () => rxQuery({ ... }), }, }, });
- Global default:typescript
globalQueries({ queries: { ... } }, { cacheTime: 120000 }); // 2 minutes
Feature flag
You can organize queries by feature using the featureName
option:
globalQueries({ queries: { ... } }, { featureName: 'user' });
This helps modularize queries and avoid key collisions.
Syntax improvement idea
It works pretty well like that, but I will surely deprecated this API, and handle all this functionalities in the incoming ServerStateStore
It may looks like that:
const { withUserQuery, injectUserQuery, withUserMutation, injectUserMutation } = serverStateStore(
{
providedIn: 'root',
persister: localStoragePersister, // optional
cacheTime: 60000, // optional 1 minute cache
},
withMutation('user', () =>
mutation({
method: (user: User) => user,
loader: async ({ params: user }) => user,
})
),
({ id }: SignalProxy<{ id: string | undefined }>) =>
withQuery(
'user',
() =>
query({
params: id,
loader: async ({ params: id }) => ({
id,
name: 'Romain',
}),
}),
() => ({
// ... react on the user mutation
})
),
withUsersServerState()
);
- While the current pattern enforce to create one globalQueries for the app, that avoids cached key collision (when used with a persister), it does not share easily mutation. And also, split the query declaration in multiples files (when reacting to mutation).
- This new pattern will enable to create fully declarative and reactive queries, that will help to create more advanced UX pattern.
- It will enable local ServerStateStore and server state composition.