TL;DR – Vergeet handmatige isLoading‑booleans en retries; TanStack Query neemt fetch‑logica, caching, retries, optimistic updates en meer uit handen zodat jij je kunt focussen op features in plaats van plumbing.
Coffee‑break coding, here we come. ☕🚀
Inhoudsopgave
Waarom nog een data‑library?
De meeste Angular‑projecten starten met een simpel recept: injecteer HttpClient, maak een RxJS‑stream, toggle een paar flags in je component en klaar. Het werkt prima tot het moment dat je applicatie tientallen calls heeft, op meerdere plekken dezelfde data nodig is en gebruikers mobiel of met haperende wifi werken. Vanaf dat punt worden je loading‑booleans moeilijk beheersbaar, verschijnen spinners op het scherm en ben je kostbare tijd kwijt aan het schrijven van steeds dezelfde boilerplate.
TanStack Query positioneert zichzelf als data state management bóven je HTTP‑laag. Het is niet nog een nieuwe state‑management tool, maar een andere manier van werken met externe data waardoor een dedicated state-management package in sommige gevallen overbodig is. Het combineert het gemak van React Query (waar het van afstamt) met Angular‑friendly signals, zodat je automatisch reactive updates krijgt zonder dat je iets hoeft te subscribe‑en of unsubscribe‑en.
Framework‑agnostisch & migratie‑vriendelijk
Hoewel deze blog focust op de Angular‑adapter, is de kern van TanStack Query een framework‑onafhankelijke TypeScript‑bibliotheek. Er bestaan first‑party wrappers voor veel populaire front‑end ecosystemen:
Framework | Package | Status |
React | @tanstack/react-query | Production‑ready |
Solid | @tanstack/solid-query | Production‑ready |
Svelte | @tanstack/svelte-query | Production‑ready |
Vue | @tanstack/vue-query | Production‑ready |
Angular | @tanstack/angular-query-experimental | Experimental (maar al heel bruikbaar) |
Wat betekent dit in de praktijk?
- Multi‑repo teams kunnen dezelfde data‑mindset delen, ongeacht of er nu React‑, Vue‑ of Angular‑apps in de mono‑repo zitten. De API‑vorm lijkt per framework sterk op elkaar, dus documentatie en code‑reviews worden een stuk eenvoudiger.
- Migreren zonder herschrijven — stap je van React naar Angular (of andersom) dan kan 80‑90 % van je query‑ en mutation‑code intact blijven. Alleen de binding naar de UI‑layer verandert.
- Micro‑frontends: elk deel van je pagina, onafhankelijk van het gekozen framework, kan dezelfde cache delen mits je een gedeelde QueryClient expose‑t. Daarmee voorkom je dubbel werk en dubbele netwerk‑requests.
Kortom: TanStack Query bepaalt niet je tech‑stack. Het is een robuuste data‑laag die meegroeit en meeverhuist, of je nu morgen Solid JS probeert of volgend jaar alles omschrijft naar Svelte.
De naïeve aanpak
Onderstaande code is herkenbaar voor elke Angular‑ontwikkelaar. We simuleren een kleine netwerkvertraging zodat je het loading‑gedrag kunt zien, vangen een mogelijke error, kopiëren de response naar component‑state en onthouden netjes of we nog bezig zijn.
private http = inject(HttpClient);
isLoading = false;
isError = false;
error: any = null;
users: any[] = [];
constructor() {
this.loadUsers().subscribe(users => {
this.users = users;
this.isLoading = false;
});
}
loadUsers(): Observable {
this.isLoading = true;
return timer(1000).pipe(
switchMap(() => this.http.get('https://reqres.in/api/users?page=1').pipe(
catchError(error => {
this.isLoading = false;
this.isError = true;
this.error = error;
return of({ data: [] });
})
)),
map((response: any) => response.data)
);
}
Dit werkt, maar het schaalt niet. Elke nieuwe endpoint betekent kopiëren‑plakken van flags, handlers en catchError‑blokken. De éne keer vergeet je isLoading terug te zetten, de andere keer vergeet je de subscription op te ruimen. En als dezelfde call op twee plekken in de UI nodig is, voert de app hem net zo vaak uit—géén caching en dus onnodig netwerkverkeer.
De stap naar TanStack Query
Installeren & configureren
npm i @tanstack/angular-query-experimental
Voeg in je bootstrap één regel toe:
import { QueryClient, provideTanStackQuery } from '@tanstack/angular-query-experimental';
bootstrapApplication(AppComponent, {
providers: [
provideTanStackQuery(new QueryClient()), // optional: .withDevtools()
],
});
Hiermee registreer je een singleton QueryClient in Angulars DI‑container. Vanaf dit moment kun je overal in je componenttree queries en mutations injecteren — zonder dat je een service hoeft te genereren.
Bestaande RxJS‑functie hergebruiken
Je bestaande loadUsers() kan blijven bestaan. De boolean flags kunnen uiteraard weg en vergeet niet de catchError te verwijderen zodat TanStack Query de error kan afvangen en verwerken. We hoeven nu de loadUsers() alleen in een query te wrappen:
import { injectQuery } from '@tanstack/angular-query-experimental';
const usersQuery = injectQuery(() => ({
queryKey: ['users'],
queryFn: () => lastValueFrom(this.loadUsers()),
}));
Je template kan nu simpelweg luisteren naar usersQuery.isLoading(), usersQuery.isError() en usersQuery.data()—alles signals, dus elke state‑change triggert automatisch een Angular change‑detection cycle.
Caching & background‑refetch
Het probleem
Stel: je hebt een productcatalogus die op zowel de home‑pagina als in een sidebar getoond wordt. In de naïeve aanpak voert ieder component z’n eigen HttpClient.get() uit. Resultaat: dubbele requests, verspilde milliseconden én een UI die telkens tussen loading en loaded flikkert wanneer je navigeert.
De TanStack‑oplossing
TanStack Query bewaart responses in een LRU‑cache. Met een staleTime geef je aan hoe lang de data vers blijft. Zolang de data niet stale is, levert TanStack Query de cached version onmiddellijk terug, waardoor je component direct wat te tonen heeft. Pas wanneer de gebruiker terugkomt naar de tab of de staleTime verlopen is, wordt in de achtergrond een stille refetch gestart. De UI blijft gevuld; als er nieuwe data komt, verschijnt die zonder vertonen van spinners.
Implementatie
productsQuery = injectQuery(() => ({
queryKey: ['products'],
queryFn: () => this.http.get('/api/products'),
staleTime: 10 * 60 * 1000, // 10 minuten
}));
Wat merkt de eindgebruiker?
Gebruikers zien vrijwel geen loading‑states meer wanneer zij door de applicatie klikken, en ze krijgen tóch actuele data zodra die beschikbaar is. Tegelijk verbruik je minder bandbreedte omdat verouderde gegevens niet onnodig opnieuw opgehaald worden.
Refetching
Als de data stale is (standaard direct; staleTime = 0) wordt automatisch een background-refetch gedaan wanneer het component opnieuw geladen wordt of als de browser pagina verborgen is geweest en weer zichtbaar wordt. Tijdens een refetch blijft de isLoading signal op false, maar wordt de isFetching signal op true gezet. Zo is het mogelijk een fetching-indicator te tonen, bijvoorbeeld een veelgebruikte dunne progress-bar-lijn bovenin beeld.
Automatische retries met exponential back‑off
Het probleem
Mobiele netwerken, hotel‑wifi en zelfs bedrijfs‑proxy’s laten je weleens in de steek. Zonder retries krijgt de gebruiker direct een foutmelding en moet hij handmatig refreshen. Met eigen RxJS‑retry‑logic eindig je met een hoop retryWhen, delay, scan en takeWhile. Naast lastig te testen is dat ook foutgevoelig — één dom foutje en je DDoS‑t de server per ongeluk.
Hoe TanStack Query het oplost
Retries zijn standaard ingeschakeld (max drie pogingen), met een exponentiële back‑off van 0 s, 0,5 s, 1 s, 2 s… Je kunt dit global of per query aanpassen. Cruciaal: network errors leiden tot retries, maar business errors (HTTP 400/403 etc.) niet, zodat je geen eindeloze loops krijgt.
Implementatievoorbeeld
Standaard hoef je niks te doen en werken retries automatisch. Je kunt echter het aantal retries (bijv: retry: 6) en de back-off vertraging (retryDelay) eenvoudig aanpassen. Eventueel kan je ook de retry logica zelf schrijven:
ordersQuery = injectQuery(() => ({
queryKey: ['orders'],
queryFn: () => this.http.get('/api/orders'),
retry: (failureCount, error) => {
// alleen retries bij offline‑ of 5xx‑situaties
if (error.status >= 500 || error.status === 0) return failureCount < 5;
return false;
},
retryDelay: attempt => Math.min(500 * 2 ** attempt, 30_000),
}));
Resultaat
Zonder extra code maak je de applicatie robuust tegen tijdelijke netwerkproblemen. Gebruikers merken dat kleine haperingen automatisch worden opgevangen en hun actie uiteindelijk toch slaagt zonder frustrerende ‘Probeer opnieuw’ pop‑ups.
Mutations, optimistic UI & cache‑invalidatie
Het probleem
Wanneer een gebruiker z’n profiel wijzigt, wil je á la minute een visuele bevestiging tonen. Tegelijk wil je niet wachten op de serverrespons, maar wel kunnen terugrollen als het misgaat. En daarna moeten alle plaatsen waar dat profiel getoond wordt opnieuw de juiste gegevens presenteren. Handmatig betekent dat: tijdelijke state opslaan, rollback‑logica schrijven, en afhankelijk van je architectuur events broadcasten of stores updaten.
TanStack‑magic
Een mutation is in TanStack Query de tegenhanger van een query. Binnen onMutate kun je de cache alvast manipuleren — optimistic update. Slaat de request fout, dan krijg je in onError de oude cache als context om automatisch te herstellen. In onSuccess kun je simpelweg invalidateQueries oproepen zodat de verse data met één regel overal ververst wordt.
Implementatie
const updateUser = injectMutation(({ queryClient }) => ({
mutationFn: (dto: UserDto) => this.http.put(`/api/users/${dto.id}`, dto),
onMutate: async (dto) => {
// 1. Snapshot vorige staat
const previous = queryClient.getQueryData(['users']);
// 2. Optimistic cache‑update
queryClient.setQueryData(['users'], users =>
users?.map(u => u.id === dto.id ? { ...u, ...dto } : u)
);
return { previous }; // context voor rollback
},
onError: (_error, _dto, ctx) => {
// 3. Herstel oude staat
queryClient.setQueryData(['users'], ctx?.previous);
},
onSuccess: () => {
// 4. Ververs gerelateerde queries
queryClient.invalidateQueries({ queryKey: ['users'] });
},
}));
Resultaat
Gebruikers zien hun wijziging onmiddellijk, zelfs op een trage verbinding. Als de server toch een fout terugstuurt, valt de UI soepel terug naar de vorige waarde — zónder dat de gebruiker iets hoeft te doen. Dit verhoogt de perceived performance aanzienlijk.
Paginated & infinite queries
Het probleem
Paginering met pure RxJS betekent state voor de huidige pagina, operators om de juiste call te doen, en logica om te voorkomen dat het scherm even leeg wordt bij elke overgang. Infinite scroll voegt daar extra complexiteit aan toe: je moet page‑parameters onthouden en progressieve arrays samenvoegen.
Oplossing
- keepPreviousData houdt de oude pagina in de cache actief terwijl de nieuwe binnenkomt – de gebruiker ziet dus nooit een lege lijst (of loader).
- injectInfiniteQuery abstraheert weg wat een volgende pageParam moet zijn en levert je paginated data in één vlakke array of in pages‑vorm, afhankelijk van je voorkeur.
Implementatie klassieke paginering
const currentPage = signal(1);
const pagedUsers = injectQuery(() => ({
queryKey: ['users', currentPage()],
queryFn: () => this.http.get(`/api/users?page=${currentPage()}`)
.pipe(map((r:any) => r.data)),
keepPreviousData: true,
}));
// event handlers
function next() { currentPage.update(p => p + 1); }
function prev() { currentPage.update(p => Math.max(1, p - 1)); }
Implementatie infinite scroll
const infiniteUsers = injectInfiniteQuery(() => ({
queryKey: ['users-infinite'],
queryFn: ({ pageParam = 1 }) => this.http
.get(`/api/users?page=${pageParam}`)
.pipe(map((r:any) => r.data)),
getNextPageParam: (_lastPage, pages) => pages.length + 1,
}));
Met infiniteUsers.fetchNextPage() laad je meer content in terwijl de gebruiker naar beneden scrolt, en infiniteUsers.hasNextPage() vertelt of er nog iets te halen valt.
Resultaat
Naadloze paginering zonder flitsende blanco schermen en zonder het complexe combineLatest van RxJS. Infinite scroll voelt alsof je een native app gebruikt, zonder merkbare vertraging tussen batches.
Initial & placeholder data
Het probleem
Skeleton‑screens zorgen voor een rustige gebruikerservaring, maar vaak heb je dubbele types nodig: één voor echte data en één voor placeholder data. Dat leidt tot if (isSkeleton) checks in je componenten en maakt unit‑tests zwaarder.
TanStack‑aanpak
Met placeholderData kun je een functie geven die onmiddellijk een shape‑identieke stub retourneert. Zodra de echte fetch klaar is, wordt de placeholder transparant vervangen. Als je data al in localStorage of via server‑side rendering beschikbaar is, kun je in plaats daarvan initialData gebruiken zodat er überhaupt geen eerste fetch meer nodig is.
Voorbeeld met skeletons
usersQuery = injectQuery(() => ({
queryKey: ['users'],
queryFn: () => lastValueFrom(this.loadUsers()),
placeholderData: () => Array.from({ length: 6 }, (_, i) => ({
id: `skeleton-${i}`,
first_name: '—', last_name: '', email: '', avatar: '',
})),
initialDataUpdatedAt: Date.now(),
}));
In je template kun je eenvoudig detecteren of het item een skeleton is (bv. id.startsWith(‘skeleton’)) en conditioneel CSS toepassen. Zodra de echte data arriveert, verdwijnen de placeholders zonder extra flags of events.
Resultaat
- No‑layout‑shift: je ontwerp springt niet.
- Geen dubbele types: skeleton en echte data delen exact hetzelfde contract.
- Minder code: één bron van waarheid.
Conclusie
TanStack Query vervangt tientallen regels herhaalde RxJS‑plumbing door declaratieve, goed gedocumenteerde helpers. Je krijgt caching, retries, background updates, optimistic UI en devtools in de browser praktisch gratis. De tijd die je anders aan infrastructuur kwijt bent, kun je nu besteden aan features.
Ready voor minder spinnerstress? 👍 Probeer TanStack Query in één van je bestaande componenten, kijk naar de devtools‑overlay en zie hoe je netwerkverkeer en UI‑flicker afnemen. Eenmaal geproefd, wil je niet meer terug.
Hoe meer je gedeelde state en caching toepast hoe eerder je tegen complexere vraagstukken aanloopt zoals lastige cache invalidation, out-of-sync state met meerdere clients etc.
Om deze en andere vraagstukken uit te leggen en een oplossing te bieden komt binnenkort een deel 2 van mijn TanStack Query blog.