resourceAsync

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:

import {resourceAsync} from 'ngx-lift';
import {Component, signal} from '@angular/core';

export class UserDetailComponent {
  userId = signal(1);

  // Automatically fetches when userId changes
  userRef = resourceAsync(() =>
    this.http.get<User>(`/api/users/${this.userId()}`)
  );
}

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 -->
<button class="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
thumbnail
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:

import {resourceAsync} from 'ngx-lift';
import {Component} from '@angular/core';

export class SearchComponent {
  searchResultsRef = resourceAsync(
    () => this.http.get(`/api/search?q=${this.query()}`),
    {lazy: true} // Won't fetch until reload() is called
  );

  search() {
    this.searchResultsRef.reload(); // Trigger fetch
  }
}

Demo: Lazy-Loaded Users

Not loaded yet (idle)

Example 3: Initial Value

Provide an initial value to display before the first fetch completes:

import {resourceAsync} from 'ngx-lift';
import {Component, inject} from '@angular/core';
import {HttpClient} from '@angular/common/http';

export class UserDetailComponent {
  private http = inject(HttpClient);

  // Provide initial value from route state or cached data
  userRef = resourceAsync(
    () => this.http.get<User>('/api/user'),
    {
      initialValue: this.getInitialUser(), // Display immediately
    }
  );

  private getInitialUser(): User | undefined {
    // Could come from router state, local storage, etc.
    return history.state?.user;
  }
}

Intermediate Examples Intermediate

Example 4: Error Handling with Fallback and Error Propagation

Handle errors with onError callback to provide fallback values, or use throwOnError: true to propagate errors:

Error Handling with Fallback

import {resourceAsync} from 'ngx-lift';
import {Component} from '@angular/core';

export class UserDetailComponent {
  userRef = resourceAsync(
    () => this.http.get<User>('/api/user'),
    {
      onError: (error) => {
        console.error('Failed:', error);
        // Return fallback value
        return {name: 'Guest User'};
      },
      onSuccess: (user) => {
        console.log('Loaded:', user);
      },
      onLoading: () => {
        console.log('Loading started');
      }
    }
  );
}

Demo: Error Handling with Fallback

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';

export class UserDetailComponent {
  private http = inject(HttpClient);
  userId = input.required<number>();

  userRef = resourceAsync(
    () => this.http.get<User>(`/api/users/${this.userId()}`),
    {
      throwOnError: true,  // Errors will propagate up
      onError: (error) => {
        console.error('API Error:', error);
        // Can still log but error will still throw
        return undefined;
      }
    }
  );
}

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

Registration Component

import {resourceAsync} from 'ngx-lift';
import {Component, inject, signal} from '@angular/core';
import {FormControl, FormGroup, Validators} from '@angular/forms';
import {HttpClient} from '@angular/common/http';

interface RegistrationPayload {
  username: string;
  password: string;
  email: string;
}

interface RegistrationResponse {
  id: number;
  username: string;
  message: string;
}

export class RegistrationComponent {
  private http = inject(HttpClient);

  // Form controls
  form = new FormGroup({
    username: new FormControl('', [Validators.required, Validators.minLength(3)]),
    password: new FormControl('', [Validators.required, Validators.minLength(8)]),
    email: new FormControl('', [Validators.required, Validators.email])
  });

  // Registration resource with exhaust + lazy
  registrationRef = resourceAsync(
    () => {
      const payload: RegistrationPayload = {
        username: this.form.value.username!,
        password: this.form.value.password!,
        email: this.form.value.email!
      };
      return this.http.post<RegistrationResponse>('/api/register', payload);
    },
    {
      lazy: true,  // Don't execute until explicitly called
      behavior: 'exhaust',  // Prevent duplicate submissions
      onSuccess: (response) => {
        console.log('Registration successful:', response);
        this.form.reset();  // Clear form on success
      },
      onError: (error) => {
        console.error('Registration failed:', error);
        // Could set form errors here
      }
    }
  );

  onSubmit() {
    if (this.form.valid) {
      this.registrationRef.execute();  // Use execute() for mutations
    }
  }
}

Registration Template

<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>
    <label for="username" class="clr-required-mark">Username</label>
    <input id="username" clrInput formControlName="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>
    <label for="email" class="clr-required-mark">Email</label>
    <input id="email" clrInput type="email" formControlName="email" placeholder="Enter your email" />
    <clr-control-error>Please enter a valid email</clr-control-error>
  </clr-input-container>

  <clr-password-container>
    <label for="password" class="clr-required-mark">Password</label>
    <input id="password" clrPassword formControlName="password" placeholder="Enter password (min 8 chars)" />
    <clr-control-error>Password must be at least 8 characters</clr-control-error>
  </clr-password-container>

  <div cds-layout="m-t:md">
    <button
      class="btn btn-primary"
      type="submit"
      [disabled]="registrationRef.isLoading()"
      [clrLoading]="registrationRef.isLoading()"
    >
      Register
    </button>
  </div>
</form>

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:

import {resourceAsync} from 'ngx-lift';
import {Component, signal} from '@angular/core';

export class UserDetailComponent {
  userId = signal(1);

  // Default 'switch' behavior - cancels previous request
  userRef = resourceAsync(
    () => this.http.get<User>(`/api/users/${this.userId()}`),
    {behavior: 'switch'} // default
  );
}

switch (default)

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';

export class SearchComponent {
  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 progress
      lazy: true
    }
  );

  search() {
    // If a search is already in progress, this will be ignored
    this.searchResultsRef.reload();
  }
}

Example 7: Status Tracking

Access granular status information with the status() signal. Use computed() for derived state:

import {resourceAsync} from 'ngx-lift';
import {Component, computed} from '@angular/core';

export class UserDetailComponent {
  userRef = resourceAsync(() => this.http.get<User>('/api/user'));

  // Use computed() instead of getter for better performance
  statusMessage = computed(() => {
    switch (this.userRef.status()) {
      case 'idle': return 'Not loaded yet';
      case 'loading': return 'Loading...';
      case 'reloading': return 'Reloading...';
      case 'resolved': return 'Data loaded!';
      case 'error': return 'Failed to load';
    }
  });
}

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

See also: computedAsync

WritableResourceRef - Local State Management Advanced

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.

import {resourceAsync} from 'ngx-lift';
import {Component, inject, signal} from '@angular/core';
import {HttpClient} from '@angular/common/http';

export class TodoComponent {
  private http = inject(HttpClient);
  todoId = signal(1);

  todoRef = resourceAsync(() => 
    this.http.get<Todo>(`/api/todos/${this.todoId()}`)
  );

  toggleTodo() {
    // 1. Update UI immediately (optimistic)
    this.todoRef.update(todo => ({ ...todo, completed: !todo.completed }));

    // 2. Send to server
    this.http.put(`/api/todos/${this.todoId()}`, this.todoRef.value())
      .subscribe({
        next: () => {
          console.log('Todo updated successfully');
        },
        error: () => {
          // Revert on error
          this.todoRef.update(todo => ({ ...todo, completed: !todo.completed }));
          alert('Failed to update todo');
        }
      });
  }
}

Demo: Todo Toggle

Example 2: Cache-First Loading

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';

export class UserProfileComponent {
  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 background
    this.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';

export class CounterComponent {
  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.

Implementation

import {resourceAsync, ResourceRef} from 'ngx-lift';
import {Component, inject, Injector, runInInjectionContext, untracked} from '@angular/core';
import {HttpClient} from '@angular/common/http';

export class ProjectListComponent {
  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 = new Map<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> {
    return untracked(() => {  // Break out of reactive context
      let orgRef = this.orgRefsMap.get(orgId);
      if (!orgRef) {
        orgRef = runInInjectionContext(this.injector, () =>  // Provide injection context
          resourceAsync(
            () => 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-spinner size="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...

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>

interface ResourceRefOptions<T, E = Error> {
  initialValue?: T;
  behavior?: 'switch' | 'exhaust';
  onError?: (error: E) => T | undefined;
  throwOnError?: boolean;
  onSuccess?: (value: T) => void;
  onLoading?: () => void;
  lazy?: boolean;
}

interface ResourceRef<T, E = Error> {
  readonly value: Signal<T>;              // Always returns T (with initialValue fallback)
  readonly error: Signal<E | null>;
  readonly status: Signal<ResourceStatus>;
  readonly isLoading: Signal<boolean>;
  readonly isIdle: Signal<boolean>;        - ngx-lift extension
  hasValue(): this is ResourceRef<Exclude<T, undefined>, E>;  // Type predicate method
  reload(): boolean;                      // Returns true if initiated, for read operations (GET)
  execute(): boolean;                     // Alias for reload() - for mutations (POST/PUT/DELETE)
}

type ResourceStatus = '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.
    • onSuccess?: (value: T) => void - Callback invoked when operation successfully completes.
    • onLoading?: () => void - Callback invoked when operation starts loading.

Returns

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).