TanStack Query (React Query): Handling HTTP Requests with EaseEntry 2 of 10

Fetching Data with useQuery

useQuery runs a query function, caches its result under a queryKey, and returns the request's state.

Notes

  • queryKey: an array that identifies the data in the cache, e.g. ['events']. Same key = same cached data, across all components.
  • queryFn: the function that fetches. It must return a Promise and throw on failure; otherwise TanStack Query treats the response as success.
  • Returned values:
ValueMeaning
dataWhat queryFn returned (undefined until loaded)
isPendingNo data yet; the first request is on its way
isFetchingAny request in progress, including background refetches
isErrortrue if queryFn threw
errorThe thrown error
refetchCall manually to run the same query again
  • Failed queries retry 3 times by default before isError becomes true.

What / Why / When

What

Declarative data fetching.

Why

Loading, error, caching, and refetching handled in one hook.

When

Any GET request whose result a component displays.

Code compare

React
// components/NewEventsSection.tsximport { useQuery } from "@tanstack/react-query";import { fetchEvents } from "../services/events";​export default function NewEventsSection() {  const { data, isPending, isError, error, refetch } = useQuery({    queryKey: ["events", { max: 3 }],    queryFn: ({ signal }) => fetchEvents({ signal, max: 3 }),  });​  if (isPending) return <LoadingIndicator />;​  if (isError) {    return (      <>        <ErrorBlock          title="An error occurred"          message={error.message || "Failed to fetch events."}        />        <button onClick={() => refetch()}>Try again</button>      </>    );  }​  return (    <ul className="events-list">      {data.map((event) => (        <li key={event.id}>          <EventItemCard event={event} />        </li>      ))}    </ul>  );}
Angular
// new-events-section.component.ts@Component({  selector: "app-new-events-section",  imports: [EventItemCardComponent, LoadingIndicatorComponent, ErrorBlockComponent],  template: `    @if (events.isLoading()) {      <app-loading-indicator />    } @else if (events.error()) {      <app-error-block title="An error occurred" message="Failed to fetch events." />      <button (click)="events.reload()">Try again</button>    } @else {      <ul class="events-list">        @for (event of events.value(); track event.id) {          <li><app-event-item-card [event]="event" /></li>        }      </ul>    }  `,})export class NewEventsSectionComponent {  private eventsService = inject(EventsService);​  protected events = rxResource({    stream: () => this.eventsService.getEvents({ max: 3 }),    defaultValue: [],  });}

Differences and gotchas

  • The course notes call it refresh; the correct name is refetch. Angular's equivalent is reload().
  • isPending is true only before the first data arrives. Use isFetching to show a background-refresh indicator.
  • rxResource state belongs to the component instance; there's no shared cache unless you add one (Entry 3).
  • In Angular before v20, rxResource used request/loader instead of params/stream.

Quick reference

ReactAngular
useQuery({ queryKey, queryFn })rxResource({ params, stream })
datavalue()
isPendingisLoading() (first load)
isError / errorerror()
refetch()reload()