EthicsComputerCourses
Join training +91 90532 10052

Async Data with resource

Angular · Lesson 21 of 41 Intermediate 6 min read Updated

Loading data is one of the most common jobs in any app: fetch a list when a page opens, fetch again when the user types a new search, show a spinner while waiting and a message when something fails. Writing this by hand with subscribe() and several signals is repetitive and easy to get wrong.

Angular's resource APIs do it for you. A resource watches some signals, loads data whenever they change, and exposes the result, the loading state and any error as signals. resource, httpResource and rxResource are all stable in Angular 22.

resource(): loading with async/await

resource() takes two main options:

  • params: a function that reads signals and returns the values the request needs. When those signals change, the resource loads again.
  • loader: an async function that receives the params and returns the data.

Here a PIN code lookup calls a backend with fetch whenever the user has typed all six digits:

TypeScript
import { Component, resource, signal } from '@angular/core';

interface Place {
  pin: string;
  district: string;
  state: string;
}

@Component({
  selector: 'app-pin-lookup',
  template: `
    <input #box [value]="pin()" (input)="pin.set(box.value)" maxlength="6" placeholder="PIN code" />

    @if (place.isLoading()) {
      <p>Looking up {{ pin() }}...</p>
    } @else if (place.error()) {
      <p class="error">Could not find that PIN code.</p>
    } @else if (place.hasValue()) {
      <p>{{ place.value().district }}, {{ place.value().state }}</p>
    } @else {
      <p>Enter all 6 digits.</p>
    }
  `,
})
export class PinLookup {
  protected pin = signal('302001');

  protected place = resource({
    params: () => (this.pin().length === 6 ? this.pin() : undefined),
    loader: async ({ params, abortSignal }) => {
      const response = await fetch(`/api/pincode/${params}`, { signal: abortSignal });
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}`);
      }
      return (await response.json()) as Place;
    },
  });
}

If the server answers { "pin": "302001", "district": "Jaipur", "state": "Rajasthan" }, the page shows:

Output
Looking up 302001...
Jaipur, Rajasthan

Some details make this robust:

  • When params returns undefined, the resource does not load at all and its status is 'idle'. That is how "wait until six digits" works without any if in the loader.
  • abortSignal is cancelled when the params change before the previous request finishes. Passing it to fetch stops old, unneeded requests, so a slow answer for an old PIN can never overwrite the answer for the new one.
  • Throwing in the loader puts the resource into the error state.

The state signals

Every resource exposes its state as signals:

Signal or methodMeaning
value()The loaded data (or the default value)
hasValue()true when a value is available
isLoading()true while a request is running
error()The error, if the last load failed
status()'idle', 'loading', 'reloading', 'resolved', 'error' or 'local'
reload()Runs the loader again with the same params
set() / update()Change the value locally, for example after an edit (status becomes 'local')

httpResource(): resources on top of HttpClient

Most Angular apps use HttpClient, with its interceptors for auth headers and logging. httpResource() creates a resource that sends its requests through HttpClient (so remember provideHttpClient()). You only describe the request, as a URL string or an object, and it is re-sent whenever the signals it reads change:

TypeScript
import { httpResource } from '@angular/common/http';
import { Component, computed, signal } from '@angular/core';

interface MenuItem {
  id: number;
  name: string;
  price: number;
}

@Component({
  selector: 'app-menu-search',
  template: `
    <input #q (input)="term.set(q.value)" placeholder="Search the menu" />

    @if (menu.error()) {
      <p class="error">Menu unavailable. <button (click)="menu.reload()">Try again</button></p>
    } @else {
      @if (menu.isLoading()) {
        <p>Loading...</p>
      }
      <ul>
        @for (item of menu.value(); track item.id) {
          <li>{{ item.name }} - ₹{{ item.price }}</li>
        } @empty {
          <li>No dishes found.</li>
        }
      </ul>
      <p>{{ count() }} dishes</p>
    }
  `,
})
export class MenuSearch {
  protected term = signal('');

  protected menu = httpResource<MenuItem[]>(
    () => ({ url: '/api/menu', params: { q: this.term() } }),
    { defaultValue: [] },
  );

  protected count = computed(() => this.menu.value().length);
}

The first request goes to /api/menu?q=. Typing "dosa" cancels any request still running and sends /api/menu?q=dosa:

Output
Loading...
No dishes found.
0 dishes

Masala Dosa - ₹80
Rava Dosa - ₹90
2 dishes

While the new request runs, value() falls back to the defaultValue (an empty list), which is why "No dishes found" briefly appears under "Loading...". defaultValue also means value() is never undefined, so count needs no extra checks. If the server fails, the error message and a "Try again" button appear; reload() sends the same request again.

httpResource.text(), httpResource.blob() and httpResource.arrayBuffer() load other response types, and an HttpResourceRef also offers statusCode() and headers() signals.

rxResource(): when you already have an observable

If your service already returns observables, as the MenuApi from HttpClient does, use rxResource from @angular/core/rxjs-interop. It takes a stream function that returns an observable instead of a promise:

TypeScript
import { Component, inject, signal } from '@angular/core';
import { rxResource } from '@angular/core/rxjs-interop';
import { MenuApi } from './menu-api';

@Component({
  selector: 'app-veg-menu',
  template: `<p>{{ vegItems.value()?.length ?? 0 }} veg dishes</p>`,
})
export class VegMenu {
  private api = inject(MenuApi);
  protected term = signal('');

  protected vegItems = rxResource({
    params: () => this.term(),
    stream: ({ params }) => this.api.search(params, true),
  });
}

When the params change, rxResource unsubscribes from the old observable for you.

Which one should I use?

  • httpResource for reading JSON from your API in a component: shortest code, uses your interceptors.
  • resource for any other async source: fetch, IndexedDB, a third-party SDK that returns promises.
  • rxResource when you already have an observable-based service.
  • Plain HttpClient calls with subscribe() (or firstValueFrom) for writes such as POST, PUT and DELETE. Resources are designed for reading data that depends on signals; do not trigger a payment or an order by changing a signal.

Common mistakes

  • Reading value() in the error state. It throws. Check error() or hasValue() first.
  • Forgetting abortSignal in a resource loader. Old requests then keep running and waste data, especially on slow mobile networks.
  • Using a resource for writes. Changing a signal should not place an order. Call HttpClient.post() directly from an event handler.
  • Making every keystroke a request. For search boxes, wait for a short pause in typing. Angular 22 adds an experimental debounced() helper for this; RxJS debounceTime (see the next lesson) is the stable option.

Key points

  • Resources load async data into signals and reload automatically when the signals in params change.
  • resource uses an async loader, httpResource uses HttpClient, and rxResource uses an observable stream.
  • Returning undefined from params keeps a resource idle; abortSignal cancels outdated requests.
  • Use isLoading(), error(), hasValue(), status() and reload() to build loading and error states.
  • defaultValue keeps value() defined; never read value() while the resource is in the error state.

Exercise

Build a TrainStatus component with a trainNo signal (start with '12301'). Use httpResource to load /api/trains/{trainNo}/status, which returns { name: string; delayMinutes: number }. Do not send a request unless the train number has exactly 5 digits. Show "Loading...", an error message with a retry button, or "NAME is running X minutes late" (or "on time" when the delay is 0).

Show solution
TypeScript
import { httpResource } from '@angular/common/http';
import { Component, signal } from '@angular/core';

@Component({
  selector: 'app-train-status',
  template: `
    <input #t [value]="trainNo()" (input)="trainNo.set(t.value)" />
    @if (status.isLoading()) {
      <p>Loading...</p>
    } @else if (status.error()) {
      <p>Status not available. <button (click)="status.reload()">Retry</button></p>
    } @else if (status.hasValue()) {
      @let s = status.value();
      <p>{{ s.name }} is {{ s.delayMinutes === 0 ? 'on time' : 'running ' + s.delayMinutes + ' minutes late' }}</p>
    }
  `,
})
export class TrainStatus {
  protected trainNo = signal('12301');
  protected status = httpResource<{ name: string; delayMinutes: number }>(() =>
    /^\d{5}$/.test(this.trainNo()) ? `/api/trains/${this.trainNo()}/status` : undefined,
  );
}

Returning undefined from the request function keeps the resource idle for incomplete numbers. The template checks error() before reading value(), so a failed request never throws.

Quick Quiz

1. What happens when the params function of a resource returns undefined?

2. Which API sends its requests through HttpClient and its interceptors?

3. What does reading value() do while a resource is in the error state?

4. Which task should NOT be done with a resource?

Learn Angular with a mentor

Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call