TanStack Query for Angular: Server State Management with Signals in 2026
Learn how TanStack Query for Angular handles server state — loading, caching, refetching, and mutations — using signals for a fully reactive, type-safe data layer.
TL;DR: TanStack Query for Angular gives you automatic caching, background refetching, optimistic mutations, and deduplication out of the box — all surfaced as Angular signals. If you are managing async server state with manual
isLoadingbooleans andhttpResource()workarounds, this library is worth a serious look.
Angular's built-in httpResource() and resource() APIs are excellent for simple reads, but TanStack Query for Angular solves the harder problems that come up as apps grow: stale data, cache invalidation, background refetching, pagination, and mutation state. If you have spent time writing boilerplate isLoading / isError / data signals by hand for every HTTP call, TanStack Query for Angular server state management collapses all of that into a single, predictable API — and it integrates directly with Angular signals.
The Problem: Server State Is Not the Same as UI State
Most Angular developers reach for a service with a BehaviorSubject or a writable signal to share data across components. That works fine for UI state — things like whether a sidebar is open or which tab is active. Server state is fundamentally different: it lives on the server, goes stale the moment it is fetched, needs to be refetched when the user focuses the tab, and has to stay in sync across multiple components that reference the same data.
Handling this manually produces a familiar pattern:
// The boilerplate every Angular dev has written a dozen times
@Injectable({ providedIn: 'root' })
export class ProductsService {
private http = inject(HttpClient);
readonly isLoading = signal(false);
readonly error = signal<string | null>(null);
readonly products = signal<Product[]>([]);
load() {
this.isLoading.set(true);
this.error.set(null);
this.http.get<Product[]>('/api/products').subscribe({
next: (data) => {
this.products.set(data);
this.isLoading.set(false);
},
error: (err) => {
this.error.set(err.message);
this.isLoading.set(false);
},
});
}
}
This service has no caching. Two components that call load() within milliseconds of each other fire two requests. There is no background refetching when the user returns to the tab. Pagination requires even more boilerplate. TanStack Query for Angular server state management replaces all of this with a single injectQuery() call.
The Concept: Query Keys, Stale Time, and the Query Cache
TanStack Query (formerly React Query) is the most popular server-state library in the JavaScript ecosystem, with over 44 million weekly npm downloads. The Angular adapter — @tanstack/angular-query-experimental — surfaces the same primitives as first-class Angular signals.
Three concepts explain most of how TanStack Query works:
Query key — a serializable array that uniquely identifies a piece of server data. ['products'] identifies the products list. ['product', 42] identifies a single product with ID 42. The cache is keyed by this array, so any component that calls injectQuery with ['products'] shares the same cached result — no duplicate requests.
Stale time — how long a query result is considered fresh. With a stale time of 30 seconds, multiple components mounting within that window all read from cache without firing a new network request. Once the data goes stale, the next mount triggers a background refetch while still showing the cached data immediately.
Query cache — a global in-memory store managed by QueryClient. It handles deduplication, background refetching, garbage collection of unused queries, and optimistic updates for mutations.
These concepts are explained in depth in the TanStack Query documentation.
The Tool: @tanstack/angular-query-experimental
Install the Angular adapter and its devtools:
npm install @tanstack/angular-query-experimental
npm install --save-dev @tanstack/angular-query-devtools-experimental
Wire up the QueryClient in your application config:
// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideAngularQuery, QueryClient } from '@tanstack/angular-query-experimental';
export const appConfig: ApplicationConfig = {
providers: [
provideAngularQuery(new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 30, // 30 seconds before background refetch
gcTime: 1000 * 60 * 5, // cache kept for 5 minutes after last use
retry: 2,
},
},
})),
],
};
Now use injectQuery() in any standalone component:
import { Component, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { injectQuery } from '@tanstack/angular-query-experimental';
import { lastValueFrom } from 'rxjs';
interface Product {
id: number;
name: string;
price: number;
}
@Component({
standalone: true,
selector: 'app-products',
template: `
@if (query.isPending()) {
<p>Loading products...</p>
} @else if (query.isError()) {
<p>Error: {{ query.error()?.message }}</p>
} @else {
@for (product of query.data(); track product.id) {
<div>{{ product.name }} — {{ product.price | currency }}</div>
}
}
`,
})
export class ProductsComponent {
private http = inject(HttpClient);
query = injectQuery(() => ({
queryKey: ['products'],
queryFn: () => lastValueFrom(this.http.get<Product[]>('/api/products')),
}));
}
query.isPending(), query.isError(), query.data(), and query.error() are all Angular signals. The template above uses the new @if / @for control flow — no legacy structural directives or async pipe needed.
Mutations with injectMutation()
Reads are only half the picture. Use injectMutation() for creates, updates, and deletes, and call queryClient.invalidateQueries() in onSuccess to trigger an automatic refetch:
import { Component, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import {
injectMutation,
injectQueryClient,
} from '@tanstack/angular-query-experimental';
import { lastValueFrom } from 'rxjs';
@Component({
standalone: true,
selector: 'app-add-product',
template: `
<button (click)="addProduct()" [disabled]="mutation.isPending()">
{{ mutation.isPending() ? 'Saving...' : 'Add Product' }}
</button>
@if (mutation.isError()) {
<p class="error">{{ mutation.error()?.message }}</p>
}
`,
})
export class AddProductComponent {
private http = inject(HttpClient);
private queryClient = injectQueryClient();
mutation = injectMutation(() => ({
mutationFn: (newProduct: Partial<Product>) =>
lastValueFrom(this.http.post<Product>('/api/products', newProduct)),
onSuccess: () => {
// Invalidate the products list so it refetches automatically
this.queryClient.invalidateQueries({ queryKey: ['products'] });
},
}));
addProduct() {
this.mutation.mutate({ name: 'New Widget', price: 19.99 });
}
}
The queryClient.invalidateQueries({ queryKey: ['products'] }) call marks the products list as stale and triggers a background refetch in any mounted component that is displaying it — no manual service calls required.
Using ZyraUI for Server State Feedback
TanStack Query for Angular pairs naturally with ZyraUI components for turning query state into polished UI. A loading spinner, a data table, and an empty state all become reactive to TanStack Query's signal-based status flags.
Here is a products table that wires zyra-spinner and zyra-table directly to query signals:
import { Component, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { injectQuery } from '@tanstack/angular-query-experimental';
import { lastValueFrom } from 'rxjs';
import { ZyraSpinnerComponent } from '@zyra-ui/angular';
import { ZyraTableComponent } from '@zyra-ui/angular';
@Component({
standalone: true,
imports: [ZyraSpinnerComponent, ZyraTableComponent],
selector: 'app-products-table',
template: `
@if (query.isPending()) {
<zyra-spinner size="lg" label="Loading products" />
} @else if (query.isError()) {
<p class="text-danger">Failed to load: {{ query.error()?.message }}</p>
} @else {
<zyra-table
[rows]="query.data() ?? []"
[columns]="columns"
/>
}
`,
})
export class ProductsTableComponent {
private http = inject(HttpClient);
columns = [
{ key: 'name', label: 'Product' },
{ key: 'price', label: 'Price' },
];
query = injectQuery(() => ({
queryKey: ['products'],
queryFn: () =>
lastValueFrom(this.http.get<Product[]>('/api/products')),
staleTime: 1000 * 60, // 1 minute
}));
}
The zyra-spinner appears only while the query is genuinely pending — not during background refetches, where the cached data remains visible. That distinction is one of the subtlest but most user-friendly features TanStack Query provides: users see stale data instantly while a silent refresh runs behind the scenes.
You can explore all 60+ free components at zyraui.dev — including cards, modals, tables, forms, and more. The ZyraUI docs also show how to integrate components with signal-based data layers.
Wrapping Up
TanStack Query for Angular brings battle-tested Angular server state management to a framework that already has excellent reactive primitives. The combination of Angular signals for reactivity and TanStack Query for caching, deduplication, and mutation management eliminates an entire category of boilerplate that Angular developers have been writing by hand for years. Install the package, provide the QueryClient, and replace your manual loading flags with injectQuery() — your components will be simpler and your users will notice the difference.
Frequently Asked Questions
What is TanStack Query for Angular and how does it differ from httpResource()?
TanStack Query for Angular is a server state management library that adds caching, background refetching, deduplication, and mutation helpers on top of any async fetch function. Angular's built-in httpResource() handles a single reactive HTTP read with signal-based state, but it does not deduplicate across components, manage a shared cache, or coordinate cache invalidation after mutations. TanStack Query solves all of those problems at the cost of a small dependency.
Does TanStack Query for Angular work with Angular signals?
Yes — the Angular adapter exposes query and mutation state entirely through Angular signals. Properties like query.isPending(), query.data(), query.error(), and mutation.isSuccess() are all signal getters, making them compatible with computed(), effect(), and Angular's @if / @for template control flow without any extra wiring.
How does TanStack Query handle caching and background refetching in Angular?
Every query is identified by a query key (a plain array). The global QueryClient stores results under that key and respects the configured staleTime. When staleTime expires, the next component mount or window focus event triggers a background refetch while still serving the cached data immediately — so users never see a blank loading screen for data they already viewed.
Is @tanstack/angular-query-experimental production ready?
The "experimental" label reflects the Angular adapter's API stability rather than the underlying TanStack Query core, which is production-hardened across millions of React, Vue, and Svelte applications. The Angular adapter API has been stable for several major versions and is widely used in production Angular applications. Check the TanStack Query release notes for the current stability status.