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 queryFn depends on into the key; otherwise different requests share one cache entry.

signal:

  • TanStack Query passes { signal, queryKey } to queryFn.
  • Pass signal to fetch. If the component unmounts or the key changes mid-request, the request is aborted.
  • This is the same AbortController cleanup 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

React
// 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.

Angular
// 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 pass signal. With HttpClient, unsubscribing cancels automatically.
  • When using resource() with fetch instead of HttpClient, the loader receives abortSignal, the direct equivalent of TanStack's signal.

Quick reference

TaskReactAngular
Dynamic keyqueryKey: ['events', { search }]params: () => this.searchTerm()
Per-item cache['events', id]rxResource per component (or cache service key)
CancellationPass signal to fetchAutomatic (unsubscribe)
fetch cancellationsignalabortSignal in resource() loader