The resourceAsync function creates a reactive resource that manages async operations with full state tracking. Similar to Angular's httpResource but works with any async operation (Observable, Promise, or sync value). It provides automatic request cancellation when dependencies change, manual reload() capability, and granular status tracking (idle → loading → resolved/error → reloading → resolved/error).
Basic Examples Beginner
Example 1: Basic Usage with Automatic Dependency Tracking
Create a resource that automatically fetches data and re-fetches when dependencies change. Any signal read inside the source function becomes a reactive dependency:
When userId() changes, resourceAsync automatically:
Cancels the previous HTTP request (if still in-flight)
Sets status to 'loading'
Makes a new request with the updated userId
Updates to 'resolved' when complete
Access individual signals in templates for optimal performance:
<!-- Loading state -->
@if (userRef.isLoading()) {
<cll-spinner />
}
<!-- Error state -->
@if (userRef.error(); as error) {
<cll-alert [error]="error" />
}
<!-- Success state -->
<!-- If you want to keep previous data while loading, you can use the following code: -->
@if (userRef.value(); as userData) {
<app-user-card [user]="userData" />
}
<!-- if you want to hide previous data while refetching, you can use the following code: -->
@if (userRef.status() === 'resolved' && userRef.value(); as userData) {
<app-user-card [user]="userData" />
}
<!-- Or check hasValue() for loading while data exists -->
@if (userRef.hasValue()) {
<app-user-card [user]="userRef.value()!" />@if (userRef.isLoading()) {
<span>Refreshing...</span>
}
}
<!-- Reload -->
<buttonclass="btn btn-sm" (click)="userRef.reload()">Reload</button>
Demo: User Profile
Whenever different button is clicked, resource will be fetched reactively. Clicking the same button won't trigger a call since the dependency doesn't change. However, you can click the reload button, which triggers resource.reload() to force refetching.
Status: resolved Loading: falseHas Value: true
Name
Stéfanie Souren
Gender
female
Example 2: Lazy Loading
Use lazy: true to defer loading until reload() is called. The resource starts in 'idle' state and won't fetch data until explicitly triggered:
This resource attempts to fetch from an invalid endpoint and falls back to a default user:
Error Propagation
Use throwOnError: true to propagate errors instead of storing them in the error signal:
import {resourceAsync} from'ngx-lift';
import {Component, inject, input} from'@angular/core';
import {HttpClient} from'@angular/common/http';
exportclassUserDetailComponent {
private http = inject(HttpClient);
userId = input.required<number>();
userRef = resourceAsync(
() =>this.http.get<User>(`/api/users/${this.userId()}`),
{
throwOnError: true, // Errors will propagate uponError: (error) => {
console.error('API Error:', error);
// Can still log but error will still throwreturnundefined;
}
}
);
}
When throwOnError: true, errors will be thrown even if onError is provided. The onError callback can still be used for logging before the error propagates.
Example 5: Mutations with execute() - User Registration Form
For mutations (POST/PUT/DELETE), use execute() method instead of reload(). The execute() method is semantically appropriate for operations like form submissions, data saves, and deletes.
Key Configuration for Mutations:
lazy: true - Don't execute until explicitly triggered
behavior: 'exhaust' - Prevent duplicate submissions (ignore new requests while one is in progress)
execute() - Use this method instead of reload() for mutations
<form clrForm [formGroup]="registrationForm" (ngSubmit)="onRegistrationSubmit()">
@if (registrationRef.hasValue()) {
<cll-alert [content]="registrationRef.value().message" [alertType]="'success'"cds-layout="m-t:md" />
}
@if (registrationRef.error(); as error) {
<cll-alert [content]="'Registration failed: ' + error" [alertType]="'danger'"cds-layout="m-t:md" />
}
<clr-input-container>
<labelfor="username"class="clr-required-mark">Username</label><inputid="username"clrInputformControlName="username"placeholder="Enter username (3-20 chars)" /><clr-control-error>Username must be 3-20 characters</clr-control-error>
</clr-input-container>
<clr-input-container><labelfor="email"class="clr-required-mark">Email</label><inputid="email"clrInputtype="email"formControlName="email"placeholder="Enter your email" /><clr-control-error>Please enter a valid email</clr-control-error></clr-input-container><clr-password-container><labelfor="password"class="clr-required-mark">Password</label><inputid="password"clrPasswordformControlName="password"placeholder="Enter password (min 8 chars)" /><clr-control-error>Password must be at least 8 characters</clr-control-error></clr-password-container><divcds-layout="m-t:md"><buttonclass="btn btn-primary"type="submit"
[disabled]="registrationRef.isLoading()"
[clrLoading]="registrationRef.isLoading()"
>
Register
</button></div>
</form>
Why this works great for mutations:
lazy: true - Form submission only happens when user clicks submit
behavior: 'exhaust' - If user spams the submit button, subsequent clicks are ignored while request is in-flight
execute() - Clearer intent than "reload" for a POST request
isLoading() - Perfect for showing loading state and disabling button
status() - Track success/error states for user feedback
Live Demo: User Registration Form
Try submitting the form below. It simulates a registration API with 70% success rate. With behavior: 'exhaust', rapid clicking the submit button is ignored while a request is in progress.
Example 6: Behaviors (switch, exhaust)
Control how multiple concurrent requests are handled:
Cancels the previous request when a new one is triggered. This is the default behavior and ensures you always get the latest data. Best for read operations (GET requests).
exhaust
Ignores new requests while one is in progress. Useful for preventing duplicate operations like rapid button clicks triggering multiple searches or paginations. Also ideal for mutations (see Example 5).
import {resourceAsync} from'ngx-lift';
import {Component, signal} from'@angular/core';
exportclassSearchComponent {
searchTerm = signal('');
// Exhaust prevents multiple concurrent searches
searchResultsRef = resourceAsync(
() =>this.http.get(`/api/search?q=${this.searchTerm()}`),
{
behavior: 'exhaust', // Ignore new searches while one is in progresslazy: true
}
);
search() {
// If a search is already in progress, this will be ignoredthis.searchResultsRef.reload();
}
}
Example 7: Status Tracking
Access granular status information with the status() signal. Use computed() for derived state:
Always use computed() instead of getters for derived state from signals. This provides better performance through memoization and integrates perfectly with Angular's reactivity system.
When to Use
Use resourceAsync when:
You're working with read operations (GET) - use reload() method for refetching
You're working with mutations (POST/PUT/DELETE) - use execute() method with lazy: true and behavior: 'exhaust'
You need local state management - use set()/update() for optimistic updates, cache-first patterns, or form drafts
You need manual trigger capability (reload() or execute())
You want explicit status tracking (idle, isLoading, reloading, resolved, error, local)
You need lazy loading (defer initial execution)
You want individual signal accessors for optimal performance
You're looking for Angular's httpResource-like functionality
You need automatic cancellation of previous requests (switch behavior)
You need to prevent duplicate submissions (exhaust behavior)
Use computedAsync when:
You want automatic reactive fetching based on dependencies
You need custom merge behaviors (merge, concat, in addition to switch/exhaust)
You want a single computed signal that re-runs automatically
You're migrating from computed() patterns
You need to combine with createAsyncState operator
WritableResourceRef - Local State Management Advanced
New Feature:resourceAsync now returns a WritableResourceRef with set(), update(), and asReadonly() methods for manual state management, matching Angular's WritableResource API!
Example 1: Optimistic Updates - Todo Toggle
Update the UI immediately before server confirmation. If the server request fails, you can revert the change. This provides instant feedback to users.
Show cached data immediately while fetching fresh data in the background. This eliminates loading spinners for users with cached content.
import {resourceAsync} from'ngx-lift';
import {Component, inject, signal} from'@angular/core';
import {HttpClient} from'@angular/common/http';
exportclassUserProfileComponent {
private http = inject(HttpClient);
userId = signal(1);
userRef = resourceAsync(
() =>this.http.get<User>(`/api/users/${this.userId()}`),
{ lazy: true }
);
loadUser() {
// 1. Set cached data immediately (from localStorage, router state, etc.)const cachedUser = localStorage.getItem('user');
if (cachedUser) {
this.userRef.set(JSON.parse(cachedUser)); // Status: local
}
// 2. Fetch fresh data in backgroundthis.userRef.reload(); // Status: reloading → resolved
}
}
// Template shows cached data immediately, updates when fresh data arrives
Demo: Cache-First User Profile
Example 3: Form Draft - Counter with Save/Discard
Allow users to edit data locally before saving to the server. They can save changes or discard them by reloading from the server.
import {resourceAsync} from'ngx-lift';
import {Component, inject} from'@angular/core';
import {HttpClient} from'@angular/common/http';
exportclassCounterComponent {
private http = inject(HttpClient);
counterRef = resourceAsync(
() =>this.http.get<number>('/api/counter'),
{ lazy: true }
);
incrementCounter() {
this.counterRef.update(count => count + 1); // Status: local
}
decrementCounter() {
this.counterRef.update(count => count - 1); // Status: local
}
saveCounter() {
this.http.put('/api/counter', { value: this.counterRef.value() })
.subscribe({
next: () => {
alert('Counter saved!');
this.counterRef.reload(); // Get server state
},
error: () =>alert('Failed to save')
});
}
discardChanges() {
this.counterRef.reload(); // Revert to server state
}
}
// Show "Unsaved changes" when status === 'local'
Demo: Editable Counter
Advanced Pattern: Nested Resources Expert
A common scenario is displaying a list of items where each item can fetch additional related data (e.g., organizations, users, details). This section demonstrates one approach to managing individual loading/error states for each row.
⚠️ Note: This example uses untracked() and runInInjectionContext() wrappers, which add complexity. For most use cases, consider the simpler hybrid approach (mixing resourceAsync for the outer list with manual HTTP + signals for inner items). See the advanced documentation for comparison and recommendations.
📖 Full Documentation: See resource-async-advanced.md for comprehensive guide with multiple approaches, decision trees, and when to use each pattern.
Key Concepts:
Outer Resource: List of items with their own loading/error state
Inner Resources: Per-item related data with individual loading/error states
Lazy Loading: Only fetch when user clicks (minimizes API calls)
Independent States: Each row manages its own spinner/error/data
Implementation
import {resourceAsync, ResourceRef} from'ngx-lift';
import {Component, inject, Injector, runInInjectionContext, untracked} from'@angular/core';
import {HttpClient} from'@angular/common/http';
exportclassProjectListComponent {
private http = inject(HttpClient);
private injector = inject(Injector);
// Outer resource: List of projects
projectsRef = resourceAsync(() =>this.http.get<Project[]>('/api/projects')
);
// Inner resources: Map of orgId -> ResourceRef<Organization>private orgRefsMap = newMap<string, ResourceRef<Organization>>();
/**
* Get or create a resource for the given orgId.
* Called from template during change detection.
*
* REQUIRES TWO WRAPPERS:
* 1. untracked() - Prevents NG0602 (effect in reactive context)
* 2. runInInjectionContext() - Prevents NG0203 (no injection context)
*/getOrgRef(orgId: string): ResourceRef<Organization> {
returnuntracked(() => { // Break out of reactive contextlet orgRef = this.orgRefsMap.get(orgId);
if (!orgRef) {
orgRef = runInInjectionContext(this.injector, () =>// Provide injection contextresourceAsync(
() =>this.http.get<Organization>(`/api/orgs/${orgId}`),
{ lazy: true }
)
);
this.orgRefsMap.set(orgId, orgRef);
}
return orgRef;
});
}
loadOrg(orgId: string) {
this.getOrgRef(orgId).reload();
}
}
<table class="table">
<tbody>
@for (project of projectsRef.value(); track project.id) {
<tr><td>{{ project.name }}</td><td>
@if (getOrgRef(project.orgId); as orgRef) {
<!-- Loading state for THIS row only -->
@if (orgRef.isLoading()) {
<cll-spinnersize="xs" />
}
<!-- Error state for THIS row only -->
@if (orgRef.error(); as error) {
<cll-alert [content]="error" [compact]="true" /><button (click)="orgRef.reload()">Retry</button>
}
<!-- Success state -->
@if (orgRef.hasValue()) {
<strong>{{ orgRef.value().name }}</strong>
}
}
</td><td><button (click)="loadOrg(project.orgId)">Load Org</button></td></tr>
}
</tbody>
</table>
Demo: Items with Organizations
Projects
Loading items...
Pattern Benefits:
✅ Minimal API calls (only fetch what's needed)
✅ Individual loading/error states per row
✅ Retry failed fetches independently
✅ Caches org data (won't refetch same orgId)
✅ Scales well for large lists
API Reference
resourceAsync
Creates a reactive resource that manages async operations with full state tracking. Returns a ResourceRef object with state signals and reload capability.
Signature
resourceAsync<T, E = Error>(
sourceFn: () =>Observable<T> | Promise<T> | T,
options?: ResourceRefOptions<T, E>
): ResourceRef<T, E>
interfaceResourceRefOptions<T, E = Error> {
initialValue?: T;
behavior?: 'switch' | 'exhaust';
onError?: (error: E) => T | undefined;
throwOnError?: boolean;
onSuccess?: (value: T) =>void;
onLoading?: () =>void;
lazy?: boolean;
}
interfaceResourceRef<T, E = Error> {
readonlyvalue: Signal<T>; // Always returns T (with initialValue fallback)readonlyerror: Signal<E | null>;
readonlystatus: Signal<ResourceStatus>;
readonlyisLoading: Signal<boolean>;
readonlyisIdle: Signal<boolean>; - ngx-lift extension
hasValue(): this is ResourceRef<Exclude<T, undefined>, E>; // Type predicate methodreload(): boolean; // Returns true if initiated, for read operations (GET)execute(): boolean; // Alias for reload() - for mutations (POST/PUT/DELETE)
}
typeResourceStatus = 'idle' | 'loading' | 'reloading' | 'resolved' | 'error';
Parameters
sourceFn: () => Observable<T> | Promise<T> | T
A function that returns an Observable, Promise, or synchronous value. This function is reactive - any signals read inside it will be tracked as dependencies, and the resource will automatically re-fetch when they change.
options?: ResourceRefOptions<T, E>
Configuration options:
initialValue?: T - The initial value for the resource before the first fetch completes.
lazy?: boolean - If true, the resource starts in 'idle' state and won't fetch until reload() is called. Defaults to false.
behavior?: 'switch' | 'exhaust' - How to handle concurrent requests. Defaults to 'switch' (cancel previous). Use 'exhaust' to ignore new requests while one is in progress.
onError?: (error: E) => T | undefined - Error handler that can provide fallback values. If it returns a value, the resource will be in 'resolved' state with that value.
throwOnError?: boolean - If true, errors will be thrown instead of stored in the error signal. Defaults to false.
A ResourceRef<T, E> object with the following properties:
value: Signal<T | undefined> - The current value of the resource.
error: Signal<E | null> - The current error, if any.
status: Signal<ResourceStatus> - The current status: 'idle', 'loading', 'reloading', 'resolved', or 'error'.
isLoading: Signal<boolean> - Signal that returns true if any fetch operation is in progress (status is 'loading' or 'reloading'). Use this to disable buttons or show loading indicators.
isIdle: Signal<boolean> - Signal that returns true if the resource is in idle state (never fetched). ngx-lift extension - not in Angular's httpResource.
hasValue(): this is ResourceRef<Exclude<T, undefined>, E> - Returns true if the resource has a value available. Type predicate: When true, TypeScript narrows value() to exclude undefined.
reload(): boolean - Manually trigger a reload of the resource. Returns true if reload was initiated, false if unnecessary. Use for read operations (GET) where "reload" makes semantic sense.
execute(): boolean - Manually trigger execution of the resource. This is an alias for reload() but with a name more appropriate for mutations (POST/PUT/DELETE). ngx-lift extension - not in Angular's httpResource.
Resource Status
idle: Initial state, operation has never been triggered (only with lazy: true). value() = undefined.
loading: Initial fetch in progress (no previous value). value() = undefined.
reloading: Refetch in progress with previous value available. value() returns previous data.
resolved: Operation completed successfully. value() contains the data.
error: Operation failed with an error. value() = undefined (previous data is cleared).
Important: When an error occurs (even during reloading), the previous value is cleared and becomes undefined. This matches Angular's httpResource behavior.