TanStack Query (React Query): Handling HTTP Requests with EaseEntry 4 of 10
Dynamic Query Keys and signal
Query keys can include variables, giving each combination its own cache entry. The queryFn receives an AbortSignal to cancel outdated requests.
Notes
Dynamic keys:
['events', { search: searchTerm }]caches each search separately:['events', { search: 'react' }]and['events', { search: 'angular' }]are different entries.['events', id]caches each event detail separately.- When a variable in the key changes, the query refetches automatically (no dependency array needed).
- Put every value the
queryFndepends on into the key; otherwise different requests share one cache entry.
signal:
- TanStack Query passes
{ signal, queryKey }toqueryFn. - Pass
signaltofetch. If the component unmounts or the key changes mid-request, the request is aborted. - This is the same
AbortControllercleanup from the HTTP chapter, handled for you.
What / Why / When
- What
Parameterized, cancellable queries.
- Why
Per-parameter caching and no race conditions from outdated responses.
- When
Search, filters, pagination, detail pages.
Code compare
// components/FindEventSection.tsximport { useRef, useState, type FormEvent } from "react";import { useQuery } from "@tanstack/react-query";export default function FindEventSection() { const searchElement = useRef<HTMLInputElement>(null); const [searchTerm, setSearchTerm] = useState<string>(); const { data, isLoading, isError, error } = useQuery({ queryKey: ["events", { search: searchTerm }], queryFn: ({ signal }) => fetchEvents({ signal, search: searchTerm }), enabled: searchTerm !== undefined, // Entry 5 }); function handleSubmit(event: FormEvent) { event.preventDefault(); setSearchTerm(searchElement.current?.value ?? ""); } let content = <p>Please enter a search term to find events.</p>; if (isLoading) content = <LoadingIndicator />; if (isError) content = <ErrorBlock title="An error occurred" message={error.message} />; if (data) { content = ( <ul className="events-list"> {data.map((event) => ( <li key={event.id}> <EventItemCard event={event} /> </li> ))} </ul> ); } return ( <section> <form onSubmit={handleSubmit}> <input type="search" placeholder="Search events" ref={searchElement} /> <button>Search</button> </form> {content} </section> );}rxResource re-runs whenever its params signal changes and unsubscribes from the previous request, which cancels the HTTP call.
// find-event-section.component.ts@Component({ selector: "app-find-event-section", imports: [EventItemCardComponent, LoadingIndicatorComponent, ErrorBlockComponent], template: ` <section> <form (ngSubmit)="search(searchInput.value)"> <input #searchInput type="search" placeholder="Search events" /> <button>Search</button> </form> @if (events.isLoading()) { <app-loading-indicator /> } @else if (events.error()) { <app-error-block title="An error occurred" message="Failed to fetch events." /> } @else if (events.hasValue()) { <ul class="events-list"> @for (event of events.value(); track event.id) { <li><app-event-item-card [event]="event" /></li> } </ul> } @else { <p>Please enter a search term to find events.</p> } </section> `,})export class FindEventSectionComponent { private eventsService = inject(EventsService); protected searchTerm = signal<string | undefined>(undefined); protected events = rxResource({ params: () => this.searchTerm(), // like a dynamic queryKey stream: ({ params: search }) => this.eventsService.getEvents({ search }), }); search(term: string) { this.searchTerm.set(term); }}Differences and gotchas
- With plain
fetch, cancellation only works if you passsignal. WithHttpClient, unsubscribing cancels automatically. - When using
resource()withfetchinstead ofHttpClient, the loader receivesabortSignal, the direct equivalent of TanStack'ssignal.
Quick reference
| Task | React | Angular |
|---|---|---|
| Dynamic key | queryKey: ['events', { search }] | params: () => this.searchTerm() |
| Per-item cache | ['events', id] | rxResource per component (or cache service key) |
| Cancellation | Pass signal to fetch | Automatic (unsubscribe) |
fetch cancellation | signal | abortSignal in resource() loader |