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

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()andrxResource()are marked@developerPreviewin 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()withlinkedSignalfor derived async state that reacts to multiple signal sources without creating separate resources. - Server-Side Rendering (SSR) with
resource()— Angular'sTransferStateintegration 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
TestBedwith a signal harness to assert loading → resolved → error state transitions without real HTTP calls.
Further Reading
- Angular Docs: resource() API (official guide)
- Angular API Reference: resource()
- Angular API Reference: rxResource()
- Angular Signals overview
- GitHub Discussion: resource() RFC and design decisions
- Angular Blog: Meet Angular's New Resource API
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
Read next
Comments (0)
Join the conversation
Sign in to leave a comment on this post.
No comments yet. to be the first!