The TanStack Query (also known as react-query) adapter for Angular applications
Get rid of granular state management, manual refetching, and async spaghetti code. TanStack Query gives you declarative, always-up-to-date auto-managed queries and mutations that improve your developer experience.
Features
✅ Backend agnostic
✅ Dedicated Devtools
✅ Auto Caching
✅ Auto Refetching
✅ Window Focus Refetching
✅ Polling/Realtime Queries
✅ Parallel Queries
✅ Dependent Queries
✅ Mutations API
✅ Automatic Garbage Collection
✅ Paginated/Cursor Queries
✅ Load-More/Infinite Scroll Queries
✅ Request Cancellation
✅ Prefetching
✅ Offline Support
✅ Data Selectors
Table of Contents
- Features
- Table of Contents
- Installation
- Queries
- Query Client
- Mutations
- Query Global Options
- Operators
- Utils
- Use Constructor DI
- Devtools
- SSR
- Created By
- Contributors ✨
Installation
npm i @ngneat/query
yarn add @ngneat/query
Queries
Query Client
Inject the QueryClientService
provider to get access to the query client instance:
import { QueryClientService } from '@ngneat/query';
@Injectable({ providedIn: 'root' })
export class TodosService {
private queryClient = inject(QueryClientService);
}
Query
Inject the UseQuery
in your service. Using the hook is similar to the official hook, except the query function should return an observable
.
import { UseQuery } from '@ngneat/query';
@Injectable({ providedIn: 'root' })
export class TodosService {
private http = inject(HttpClient);
private useQuery = inject(UseQuery);
getTodos() {
return this.useQuery(['todos'], () => {
return this.http.get<Todo[]>(
'https://jsonplaceholder.typicode.com/todos'
);
});
}
getTodo(id: number) {
return this.useQuery(['todo', id], () => {
return this.http.get<Todo>(
`https://jsonplaceholder.typicode.com/todos/${id}`
);
});
}
}
Use it in your component:
import { SubscribeModule } from '@ngneat/subscribe';
@Component({
standalone: true,
imports: [NgIf, NgForOf, SpinnerComponent, SubscribeModule],
template: `
<ng-container *subscribe="todos$ as todos">
<ng-query-spinner *ngIf="todos.isLoading"></ng-query-spinner>
<p *ngIf="todos.isError">Error...</p>
<ul *ngIf="todos.isSuccess">
<li *ngFor="let todo of todos.data">
{{ todo.title }}
</li>
</ul>
</ng-container>
`,
})
export class TodosPageComponent {
todos$ = inject(TodosService).getTodos().result$;
}
Note that using the *subscribe
directive is optional. Subscriptions can be made using any method you choose.
Infinite Query
Inject the UseInfiniteQuery
provider in your service. Using the hook is similar to the official hook, except the query function should return an observable
.
import { UseInfiniteQuery } from '@ngneat/query';
@Injectable({ providedIn: 'root' })
export class ProjectsService {
private useInfiniteQuery = inject(UseInfiniteQuery);
getProjects() {
return this.useInfiniteQuery(
['projects'],
({ pageParam = 0 }) => {
return getProjects(pageParam);
},
{
getNextPageParam(projects) {
return projects.nextId;
},
getPreviousPageParam(projects) {
return projects.previousId;
},
}
);
}
}
Checkout the complete example in our playground.
Persisted Query
Use the UsePersistedQuery
provider when you want to use the keepPreviousData
feature. For example, to implement the pagination functionality:
import { inject, Injectable } from '@angular/core';
import {
UsePersistedQuery,
QueryClientService,
queryOptions,
} from '@ngneat/query';
import { firstValueFrom } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class PaginationService {
private queryClient = inject(QueryClientService);
getProjects = inject(UsePersistedQuery)((queryKey: ['projects', number]) => {
return queryOptions({
queryKey,
queryFn: ({ queryKey }) => {
return fetchProjects(queryKey[1]);
},
});
});
prefetch(page: number) {
return this.queryClient.prefetchQuery(['projects', page], () =>
firstValueFrom(fetchProjects(page))
);
}
}
Checkout the complete example in our playground.
Mutations
Mutation Result
The official mutation
function can be a little verbose. Generally, you can use the following in-house simplified implementation.
import { HttpClient } from '@angular/common/http';
import { inject, Injectable } from '@angular/core';
import { QueryClientService } from '@ngneat/query';
@Injectable({ providedIn: 'root' })
export class TodosService {
private http = inject(HttpClient);
private queryClient = inject(QueryClientService);
addTodo({ title }: { title: string }) {
return this.http.post<{ success: boolean }>(`todos`, { title }).pipe(
tap((newTodo) => {
// Invalidate to refetch
this.queryClient.invalidateQueries(['todos']);
// Or update manually
this.queryClient.setQueryData<TodosResponse>(
['todos'],
addEntity('todos', newTodo)
);
})
);
}
}
And in the component:
import { QueryClientService, useMutationResult } from '@ngneat/query';
@Component({
template: `
<input #ref />
<button
(click)="addTodo({ title: ref.value })"
*subscribe="addTodoMutation.result$ as addTodoMutation"
>
Add todo {{ addTodoMutation.isLoading ? 'Loading' : '' }}
</button>
`,
})
export class TodosPageComponent {
private todosService = inject(TodosService);
addTodoMutation = useMutationResult();
addTodo({ title }) {
this.todosService
.addTodo({ title })
.pipe(this.addTodoMutation.track())
.subscribe();
}
}
Mutation
You can use the original mutation
functionality if you prefer.
import { HttpClient } from '@angular/common/http';
import { inject, Injectable } from '@angular/core';
import { QueryClientService, UseMutation } from '@ngneat/query';
@Injectable({ providedIn: 'root' })
export class TodosService {
private http = inject(HttpClient);
private queryClient = inject(QueryClientService);
private useMutation = inject(UseMutation);
addTodo() {
return this.useMutation(({ title }: { title: string }) => {
return this.http.post<{ success: boolean }>(`todos`, { title }).pipe(
tap((newTodo) => {
// Invalidate to refetch
this.queryClient.invalidateQueries(['todos']);
// Or update manually
this.queryClient.setQueryData<TodosResponse>(
['todos'],
addEntity('todos', newTodo)
);
})
);
});
}
}
And in the component:
@Component({
template: `
<input #ref />
<button
(click)="addTodo({ title: ref.value })"
*subscribe="addTodoMutation.result$ as addTodoMutation"
>
Add todo {{ addTodoMutation.isLoading ? 'Loading' : '' }}
</button>
`,
})
export class TodosPageComponent {
private todosService = inject(TodosService);
addTodoMutation = this.todosService.addTodo();
addTodo({ title }) {
this.addTodoMutation$.mutate({ title }).then((res) => {
console.log(res.success);
});
}
}
Query Global Options
You can provide the QUERY_CLIENT_OPTIONS
provider to set the global options of the query client instance:
import { provideQueryClientOptions } from '@ngneat/query';
{
providers: [
provideQueryClientOptions({
defaultOptions: {
queries: {
staleTime: 3000,
},
},
}),
];
}
Operators
import {
filterError,
filterSuccess,
selectResult,
mapResultData,
mapResultsData,
tapSuccess,
tapError,
startWithQueryResult,
} from '@ngneat/query';
export class TodosPageComponent {
public todos: Array<Todo>;
todosService = inject(TodosService);
ngOnInit() {
this.todosService.getTodos().result$.pipe(filterError());
this.todosService.getTodos().result$.pipe(filterSuccess());
this.todosService
.getTodos()
.result$.pipe(selectResult((result) => result.data.foo));
// Map the result `data`
this.todosService.getTodos().pipe(
mapResultData((data) => {
return {
todos: data.todos.filter(predicate),
};
})
);
// process error or success result directly
this.todosService
.getTodos()
.result$.pipe(
tapSuccess((data) => {
// get result data directly if success
this.todos = data;
}),
tapError((error) => {
// do what you want with error (display modal, toast, etc)
alert(error.message);
})
)
.subscribe();
}
}
Utils
Implementation of isFetching and isMutating.
import {
UseIsFetching,
UseIsMutating,
createSyncObserverResult
} from '@ngneat/query';
// How many queries are fetching?
const isFetching$ = inject(UseIsFetching)();
// How many queries matching the posts prefix are fetching?
const isFetchingPosts$ = inject(UseIsFetching)(['posts']);
// How many mutations are fetching?
const isMutating$ = inject(UseIsMutating)();
// How many mutations matching the posts prefix are fetching?
const isMutatingPosts$ = inject(UseIsMutating)(['posts']);
// Create sync successful observer in case we want to work with one interface
of(createSyncObserverResult(data, options?))
Use Constructor DI
You can use the constructor
version instead of inject
:
QueryService.use(...)
PersistedQueryService.use(...)
InfiniteQueryService.use(...)
MutationService.use(...)
Devtools
Install the @ngneat/query-devtools
package. Lazy load and use it only in development
environment:
import { ENVIRONMENT_INITIALIZER } from '@angular/core';
import { environment } from './environments/environment';
import { QueryClientService } from '@ngneat/query';
bootstrapApplication(AppComponent, {
providers: [
environment.production
? []
: {
provide: ENVIRONMENT_INITIALIZER,
multi: true,
useValue() {
const queryClient = inject(QueryClientService);
import('@ngneat/query-devtools').then((m) => {
m.ngQueryDevtools({ queryClient });
});
},
},
],
});
SSR
On the Server:
import { provideQueryClient } from '@ngneat/query';
import { QueryClient, dehydrate } from '@tanstack/query-core';
import { renderApplication } from '@angular/platform-server';
async function handleRequest(req, res) {
const queryClient = new QueryClient();
let html = await renderApplication(AppComponent, {
providers: [provideQueryClient(queryClient)],
});
const queryState = JSON.stringify(dehydrate(queryClient));
html = html.replace(
'</body>',
`<script>window.__QUERY_STATE__ = ${queryState}</script></body>`
);
res.send(html);
queryClient.clear();
}
Client:
import { importProvidersFrom } from '@angular/core';
import { bootstrapApplication, BrowserModule } from '@angular/platform-browser';
import { provideQueryClient } from '@ngneat/query';
import { QueryClient, hydrate } from '@tanstack/query-core';
const queryClient = new QueryClient();
const dehydratedState = JSON.parse(window.__QUERY_STATE__);
hydrate(queryClient, dehydratedState);
bootstrapApplication(AppComponent, {
providers: [
importProvidersFrom(
BrowserModule.withServerTransition({ appId: 'server-app' })
),
provideQueryClient(queryClient),
],
});
Created By
Netanel Basal |
Contributors ✨
Thank goes to all these wonderful people who contributed ❤️