DEV Community

Soumaya Erradi
Soumaya Erradi

Posted on

Angular’s Resource API: Making async state human again

Every Angular developer has lived this moment.

You fetch data.
You handle loading.
You handle errors.
You add reload logic.

And suddenly your component is 100 lines long.

Not because the feature is complex, but because async state is.

Angular’s Resource API is a signal-first solution to a problem we’ve been solving manually for years.

It doesn’t replace RxJS. It doesn’t hide async.

It makes async state behave like a signal.

The pain of async state in Angular

Before signals, async code in Angular usually meant:

  • HttpClient returning Observables
  • the async pipe in templates
  • manual flags like isLoading, hasError, isComplete
  • Subjects to trigger reloads
  • takeUntil(destroy$) to avoid memory leaks

Even with the async pipe, most components still had to juggle three states:

  • Loading – we’re waiting for data
  • Success – we have data
  • Error - something went wrong

A very typical template looked like this:

<div *ngIf="data$ | async as data; else loading">
  <ng-container *ngIf="!error; else errorBlock">
    {{ data.name }}
  </ng-container>
</div>

<ng-template #loading>Loading…</ng-template>
<ng-template #errorBlock>Error loading data</ng-template>
Enter fullscreen mode Exit fullscreen mode

It works but it’s verbose, repetitive and fragile.
Every new async operation adds more flags and more mental overhead.

What if async behaved like a signal?

Signals changed how we write synchronous state in Angular:

  • reactive by default
  • automatically tracked
  • declarative
  • no subscriptions

At some point, the obvious question emerged:

What if async code behaved like computed()?

The Resource API is Angular’s answer.

What is the Resource API?

The Resource API is a signal-first abstraction for async state.

You define:

  • reactive parameters
  • a loader function that returns a Promise or an Observable

Angular gives you:

  • a single reactive resource
  • built-in loading and error tracking
  • automatic cancellation
  • automatic teardown
  • dependency tracking on signals

No subscribe().
No async pipe.
No manual flags.

Your first resource

Here’s the simplest example using the official API shape:

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

@Component({
  selector: 'app-user',
  template: `...`
})
export class UserComponent {
  userId = signal(1);

  user = resource({
    params: () => this.userId(),
    loader: ({ params }) =>
      fetch(`/api/users/${params}`).then(r => r.json())
  });
}
Enter fullscreen mode Exit fullscreen mode

user is now one reactive unit that represents:

  • the current value
  • whether it’s loading
  • whether it failed

Using a resource in the template

Resources expose signal helpers instead of forcing you to manage flags manually.

@if (user.isLoading()) {
  <p>Loading…</p>
}
@else if (user.error()) {
  <p>{{ user.error()?.message }}</p>
}
@else {
  <h2>{{ user.value().name }}</h2>
}
Enter fullscreen mode Exit fullscreen mode

Important:
value() throws if the resource isn’t ready.
Always check isLoading() or error() first.

Reactive dependencies: the core idea

Resources automatically track signals used inside params().

userId = signal(1);

user = resource({
  params: () => this.userId(),
  loader: ({ params }) =>
    this.http.get(`/api/users/${params}`)
});
Enter fullscreen mode Exit fullscreen mode

When userId changes:

  • the resource invalidates
  • the loader re-runs
  • the UI updates automatically

No switchMap.
No Subjects.
No wiring code.

This is the key mental shift:

The resource reacts to signal changes, not events.

Reloading data manually

Sometimes you want an explicit refresh.

<button (click)="user.reload()">Reload</button>
Enter fullscreen mode Exit fullscreen mode

That’s it.

The resource automatically transitions through a loading state again.

The three flavors of Resource

Angular provides three helpers for different use cases.

resource() – the general-purpose tool

Use it when:

  • you want full control
  • you return a Promise or Observable
  • the async logic isn’t HTTP-specific
todos = resource({
  loader: () => this.http.get('/api/todos')
});
Enter fullscreen mode Exit fullscreen mode

httpResource() – HTTP without boilerplate

httpResource() is a specialized helper built on top of resource():

  • unwraps the HTTP response
  • handles HttpErrorResponse
  • integrates cleanly with HttpClient
import { httpResource } from '@angular/common/http';

user = httpResource(() =>
  this.http.get<User>('/api/user')
);
Enter fullscreen mode Exit fullscreen mode

Same mental model, fewer lines.

rxResource() – real-time streams

For data that emits multiple values over time:

  • WebSockets
  • Server-Sent Events
  • live prices
  • sensor data
import { rxResource } from '@angular/core/rxjs-interop';

updates = rxResource(() =>
  new EventSource('/api/events')
);
Enter fullscreen mode Exit fullscreen mode

The resource value updates whenever the stream emits.
Connection and teardown are handled automatically.

Before vs After: a real comparison

Before (classic RxJS setup)

private destroy$ = new Subject<void>();
private reload$ = new Subject<void>();

category$ = new BehaviorSubject('all');
isLoading = false;

products$ = this.category$.pipe(
  switchMap(cat => {
    this.isLoading = true;
    return this.http.get(`/api/products?cat=${cat}`).pipe(
      finalize(() => this.isLoading = false)
    );
  }),
  takeUntil(this.destroy$)
);
Enter fullscreen mode Exit fullscreen mode

After (Resource API)

category = signal('all');

products = httpResource(() => {
  const cat = this.category();
  return this.http.get(`/api/products?cat=${cat}`);
});
Enter fullscreen mode Exit fullscreen mode

One resource.
No flags.
No Subjects.
No teardown logic.

Common gotchas

Reading signals outside params()

const id = this.userId();

user = resource({
  params: () => id, // not reactive
  loader: ({ params }) => ...
});
Enter fullscreen mode Exit fullscreen mode

Always read signals inside params().

Accessing value() too early

@if (user.isLoading()) {
  {{ user.value() }} <!-- will throw -->
}
Enter fullscreen mode Exit fullscreen mode

Always guard access to value().

Does this replace RxJS?

No, and it shouldn’t.

  • RxJS is excellent for streams and orchestration
  • Resource is excellent for async state

A good rule of thumb:

  • RxJS for flows
  • Resource for state

They work best together.

Where should a resource live?

  • In a component Perfect for local, view-specific async state
  • In a service Great for shared or global state. Resources are signals, so they’re injectable and composable.

Final thoughts

The Resource API doesn’t kill RxJS.

It removes the noise:

  • fewer pipes
  • fewer flags
  • built-in loading and error handling
  • signal-native async logic

Async state becomes predictable, reactive and boring and that’s a compliment.

If you’re already using signals, the Resource API is the missing piece.

Async, finally made human.

Top comments (0)