Angular's Experimental resource() API: Declarative Async Data Fetching with Signals
Angular Signals TypeScript RxJS Web Development

Angular's Experimental resource() API: Declarative Async Data Fetching with Signals

D. Rout

D. Rout

May 29, 2026 9 min read

On this page

Angular's Experimental resource() API: Declarative Async Data Fetching with Signals

Managing async data in Angular has always required assembling a cocktail of BehaviorSubject, switchMap, takeUntilDestroyed, and manual loading/error flags. Angular 19 ships an experimental answer to all of that: the resource() API. It wraps an async loader in a signal-aware container that automatically tracks loading state, surfaces errors, and re-runs whenever its reactive dependencies change.

In this tutorial we'll build a small post-browsing app against the JSONPlaceholder API to explore both resource() (fetch-based) and rxResource() (Observable-based). The complete working project is on GitHub: angular-resource-api-demo.

⚠️ Experimental status. resource() and rxResource() are marked @developerPreview in Angular 19. The API surface may change before it graduates to stable. Use in production with that caveat in mind.


Prerequisites

  • Node.js 20 or later
  • Angular CLI 19+ (`npm i -g @angular/cli@19`)
  • Familiarity with Angular standalone components and basic Signals
  • Optional: comfort with RxJS Observables for the rxResource() section

1. What Problem Does resource() Solve?

Before resource(), fetching data reactively looked something like this:

// The old way — lots of moving parts
@Component({ ... })
export class PostListComponent implements OnInit {
  posts$ = new BehaviorSubject<Post[]>([]);
  isLoading = false;
  error: unknown = null;

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

  ngOnInit(): void {
    this.userId$
      .pipe(
        tap(() => (this.isLoading = true)),
        switchMap(id => this.postsService.getPosts(id)),
        takeUntil(this.destroy$)
      )
      .subscribe({
        next: posts => {
          this.posts$.next(posts);
          this.isLoading = false;
        },
        error: err => {
          this.error = err;
          this.isLoading = false;
        },
      });
  }

  ngOnDestroy(): void {
    this.destroy$.next();
  }
}

That's around 25 lines to do something conceptually simple. With resource():

postsResource = resource({
  request: () => ({ userId: this.selectedUserId() }),
  loader: async ({ request: { userId } }) =>
    firstValueFrom(this.postsService.getPosts(userId)),
});

The resource manages loading, error, and value states automatically, re-executes when selectedUserId changes, and tears itself down with the component. Zero manual subscriptions.


2. Project Setup

ng new angular-resource-api-demo --standalone --routing
cd angular-resource-api-demo

Enable HttpClient in app.config.ts:

// src/app/app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
    provideHttpClient(),
  ],
};

No BrowserModule, no HttpClientModule import — just the function-based providers introduced in Angular 15+.


3. Define Your Data Models

// src/app/models/post.model.ts
export interface Post {
  id: number;
  title: string;
  body: string;
  userId: number;
}

export interface User {
  id: number;
  name: string;
  email: string;
  username: string;
}

Clean typed models let the resource infer its value type automatically.


4. Create a Data Service

// src/app/services/posts.service.ts
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { Post, User } from '../models/post.model';

const API_BASE = 'https://jsonplaceholder.typicode.com';

@Injectable({ providedIn: 'root' })
export class PostsService {
  private http = inject(HttpClient);

  getPosts(userId?: number): Observable<Post[]> {
    const url = userId
      ? `${API_BASE}/posts?userId=${userId}`
      : `${API_BASE}/posts`;
    return this.http.get<Post[]>(url);
  }

  getPost(id: number): Observable<Post> {
    return this.http.get<Post>(`${API_BASE}/posts/${id}`);
  }
}

The service stays plain Observable-returning — we bridge to resource() at the component level, which keeps the service reusable.


5. Using resource() with a Fetch Loader

resource() is imported from @angular/core. Its loader function is an async function that returns a Promise.

// src/app/components/post-list/post-list.component.ts
import { Component, inject, signal, resource } from '@angular/core';
import { CommonModule } from '@angular/common';
import { PostsService } from '../../services/posts.service';
import { firstValueFrom } from 'rxjs';

@Component({
  selector: 'app-post-list',
  standalone: true,
  imports: [CommonModule],
  templateUrl: './post-list.component.html',
})
export class PostListComponent {
  private postsService = inject(PostsService);

  // Signal that drives the resource request
  selectedUserId = signal<number | undefined>(undefined);

  postsResource = resource({
    // Called reactively — any signal read here becomes a dependency
    request: () => ({ userId: this.selectedUserId() }),
    loader: async ({ request: { userId }, abortSignal }) => {
      return firstValueFrom(this.postsService.getPosts(userId));
    },
  });

  // Convenient aliases
  posts = this.postsResource.value;
  isLoading = this.postsResource.isLoading;
  error = this.postsResource.error;

  filterByUser(userId: number | undefined): void {
    this.selectedUserId.set(userId);   // triggers automatic reload
  }

  retry(): void {
    this.postsResource.reload();       // manual reload
  }
}

Key properties exposed by ResourceRef

Property Type Description
value Signal<T | undefined> The resolved value, or undefined while loading
isLoading Signal<boolean> true while the loader Promise is in flight
error Signal<unknown> Holds the thrown error, undefined on success
status Signal<ResourceStatus> Enum: Idle, Loading, Refreshing, Resolved, Error, Local
reload() () => boolean Imperatively re-run the loader
set(value) (value: T) => void Write a local value without re-fetching (optimistic updates)
update(fn) (fn: (old) => T) => void Update local value based on previous
hasValue() () => boolean true if value is currently set (even stale)
destroy() () => void Cancel in-flight request and clean up

Template

<!-- post-list.component.html -->
<div class="container">
  <h2>Posts</h2>

  <div class="filters">
    <button (click)="filterByUser(undefined)">All Users</button>
    @for (uid of [1, 2, 3]; track uid) {
      <button (click)="filterByUser(uid)">User {{ uid }}</button>
    }
  </div>

  @if (isLoading()) {
    <div class="skeleton-list">
      @for (item of [1,2,3,4,5]; track item) {
        <div class="skeleton-card"></div>
      }
    </div>
  } @else if (error()) {
    <div class="error-state">
      <p>Something went wrong. <button (click)="retry()">Retry</button></p>
    </div>
  } @else {
    <ul class="post-list">
      @for (post of posts(); track post.id) {
        <li class="post-card">
          <h3>{{ post.title }}</h3>
          <p>{{ post.body }}</p>
        </li>
      }
    </ul>
  }
</div>

The @if/@for control flow syntax (stable since Angular 17) pairs naturally with signal-based resources.


6. Using rxResource() for Observable Loaders

If you're already deep in RxJS, rxResource() lets you use an Observable loader directly — no firstValueFrom bridge needed. It lives in @angular/core/rxjs-interop.

// src/app/components/post-detail/post-detail.component.ts
import { Component, inject, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
import { rxResource } from '@angular/core/rxjs-interop';
import { PostsService } from '../../services/posts.service';

@Component({
  selector: 'app-post-detail',
  standalone: true,
  imports: [CommonModule],
  templateUrl: './post-detail.component.html',
})
export class PostDetailComponent {
  private postsService = inject(PostsService);

  postId = signal(1);

  // loader returns an Observable — rxResource handles subscription lifecycle
  postResource = rxResource({
    request: () => this.postId(),
    loader: ({ request: id }) => this.postsService.getPost(id),
  });

  post = this.postResource.value;
  isLoading = this.postResource.isLoading;
  error = this.postResource.error;

  nextPost(): void { this.postId.update(id => id + 1); }
  prevPost(): void { this.postId.update(id => Math.max(1, id - 1)); }
}
<!-- post-detail.component.html -->
<div class="container">
  <h2>Post Detail (rxResource)</h2>

  <div class="navigation">
    <button (click)="prevPost()" [disabled]="postId() === 1">← Prev</button>
    <span>Post #{{ postId() }}</span>
    <button (click)="nextPost()">Next →</button>
  </div>

  @if (isLoading()) {
    <div class="skeleton-card tall"></div>
  } @else if (error()) {
    <p class="error">Failed to load post.</p>
  } @else if (post(); as p) {
    <div class="post-card">
      <h3>{{ p.title }}</h3>
      <p>{{ p.body }}</p>
      <small>User ID: {{ p.userId }}</small>
    </div>
  }
</div>

Every time postId updates, rxResource cancels the previous Observable (via takeUntil internally) and starts a fresh one. Zero manual unsubscribe logic.


7. resource() vs rxResource() — When to Use Which

Concern resource() rxResource()
Loader type async / Promise RxJS Observable
Import path @angular/core @angular/core/rxjs-interop
Cancellation Native AbortSignal via loader params Auto-unsubscribes previous Observable
Best for fetch(), plain async functions HttpClient, complex RxJS pipelines
RxJS dependency Optional Required
Server-side rendering
Optimistic updates ✅ via .set() ✅ via .set()

8. ResourceStatus Enum Reference

Angular exposes a ResourceStatus enum you can use for more granular UI control:

import { ResourceStatus } from '@angular/core';

// In your template or component logic:
// ResourceStatus.Idle       — resource created, loader never ran (request() returned undefined)
// ResourceStatus.Loading    — first load in progress
// ResourceStatus.Refreshing — a subsequent load triggered by request() change or .reload()
// ResourceStatus.Resolved   — value is fresh
// ResourceStatus.Error      — loader threw or rejected
// ResourceStatus.Local      — value was written locally via .set() or .update()
Status isLoading() hasValue() Typical use
Idle false false Show empty state
Loading true false Full-screen skeleton
Refreshing true true Keep showing stale data + spinner
Resolved false true Render data
Error false false Show error + retry
Local false true Optimistic update applied

9. Optimistic Updates with .set()

resource() supports local writes, making optimistic UI patterns clean:

likePost(postId: number): void {
  // Immediately update UI
  this.postsResource.update(posts =>
    posts?.map(p =>
      p.id === postId ? { ...p, liked: true } : p
    )
  );

  // Fire and forget — rollback on error if needed
  this.postsService.likePost(postId).subscribe({
    error: () => this.postsResource.reload(), // revert by reloading
  });
}

What's Next

  • Combine resource() with linkedSignal for derived async state that reacts to multiple signal sources without creating separate resources.
  • Server-Side Rendering (SSR) with resource() — Angular's TransferState integration means resource values resolved on the server can hydrate the client without a second network round-trip. Worth a dedicated deep-dive.
  • Error retry strategies — wrap the loader in a retryWhen / exponential backoff pattern and expose retry count as a signal for progressive UI feedback.
  • Testing resources — use TestBed with a signal harness to assert loading → resolved → error state transitions without real HTTP calls.

Further Reading


Wrapping Up

The resource() API is a meaningful step toward a world where Angular components declare what data they need rather than how to orchestrate fetching it. With signal reactivity, automatic cancellation, and a built-in state machine covering loading/error/refreshing, it removes a category of boilerplate that has lived in Angular apps for years.

It's experimental for a reason — the API may still shift — but the direction is solid and worth getting familiar with now. Clone the repo, run it, and start replacing your next BehaviorSubject pattern with a resource.

Full sample project: github.com/deepakrout/angular-resource-api-demo

Share

Comments (0)

Join the conversation

Sign in to leave a comment on this post.

No comments yet. to be the first!