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.
✅ 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
npm i @ngneat/query
yarn add @ngneat/query
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);
}
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.
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.
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.
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();
}
}
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);
});
}
}
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,
},
},
}),
];
}
import {
filterError,
filterSuccess,
selectResult,
mapResultData,
} from '@ngneat/query';
export class TodosPageComponent {
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),
};
})
);
}
}
The library exposes the addEntity
, and removeEntity
helpers:
import {
addEntity,
QueryClientService,
UseQuery,
removeEntity,
} from '@ngneat/query';
import { tap } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class TodosService {
private useQuery = inject(UseQuery);
private queryClient = inject(QueryClientService);
private http = inject(HttpClient);
createTodo(body) {
return this.http.post('todos', body).pipe(
tap((newTodo) => {
this.queryClient.setQueryData<TodosResponse>(
['todos'],
addEntity('todos', newTodo)
);
})
);
}
}
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(UseIsMutating)(['posts']);
// How many mutations are fetching?
const isMutating$ = inject(UseIsFetching)();
// How many mutations matching the posts prefix are fetching?
const isMutatingPosts$ = inject(UseIsMutating)(['posts']);
// Create sync successfull observer in case we want to work with one interface
of(createSyncObserverResult(data, options?))
You can use the constructor
version instead of inject
:
QueryService.use(...)
PersistedQueryService.use(...)
InfiniteQueryService.use(...)
MutationService.use(...)
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 });
});
},
},
],
});
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),
],
});
Netanel Basal |
Thank goes to all these wonderful people who contributed ❤️