computedAsync

The computedAsync function is a versatile utility enabling value computation from Promises, Observables, or regular values. It provides flexibility in computational behavior through selectable flattening strategies (switch, merge, concat, exhaust) and initial value assignment. To utilize computedAsync, provide a function returning a Promise, Observable, or regular value. It then returns a Signal emitting the computed value.

Examples

Example 1: Works with Promises

Compute values from Promises:

import {computedAsync} from 'ngx-lift';
import {Component, input, Signal} from '@angular/core';

export class UserDetailComponent {
  userId = input.required<number>();

  user: Signal<User | undefined> = computedAsync(
    () => fetch(`https://localhost/api/users/${this.userId()}`).then((res) => res.json()),
  );
}

Example 2: Works with Observables

When returning an Observable, it is automatically subscribed to and will be unsubscribed when the component is destroyed or when the computation is re-triggered. In the following example, if the userId changes, the previous computation will be cancelled (including any ongoing API calls), and a new computation will be initiated.

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

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

  user: Signal<User | undefined> = computedAsync(
    () => this.http.get<User>(`https://localhost/api/users/${this.userId()}`),
  );
}

Example 3: Works with Regular Values

This doesn't offer any advantage over employing a standard computed signal. However, it does allow the return of regular values (those not consisting of Promises or Observables) from the callback function. The callback function executes in the microtask queue, hence it won't promptly emit the value (by default, it returns undefined). To emit the value immediately, you can utilize the requireSync option within the second argument options object.

import {computedAsync} from 'ngx-lift';
import {Component, Signal} from '@angular/core';

export class UserDetailComponent {
  user: Signal<User> = computedAsync(() => ({name: 'Great user!'}), {requireSync: true});
}

Example 4: Works with initialValue

If we wish to establish an initial value for the computed value, we can provide it as the second argument within the options object.

import {computedAsync} from 'ngx-lift';
import {Component, input, Signal} from '@angular/core';

export class UserDetailComponent {
  userId = input.required<number>();

  user: Signal<User> = computedAsync(
    () => fetch(`https://localhost/api/users/${this.userId()}`).then((res) => res.json()),
    {initialValue: {name: 'Placeholder'}},
  );
}

Example 5: Works with requireSync

If we possess an Observable that synchronously emits the value, we can utilize the requireSync option to ensure immediate emission of the value. This feature is also beneficial for ensuring the signal type excludes undefined by default. Without requireSync, the signal type would be Signal<User | undefined>. However, with requireSync enabled, the signal type will be Signal<User>.

import {computedAsync} from 'ngx-lift';
import {Component, inject, input, Signal} from '@angular/core';
import {HttpClient} from '@angular/common/http';
import {startWith} from 'rxjs/operators';

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

  user: Signal<User> = computedAsync(
    () => this.http.get<User>(`https://localhost/api/users/${this.userId()}`).pipe(startWith({name: 'Placeholder'})),
    {requireSync: true},
  );
}

Example 6: Work with createAsyncState Operator

In the example below, we have a Signal that represents the state of an API call. We use computedAsync and createAsyncState operator to compute the state of the API call based on the userId.

import {computedAsync, createAsyncState} from 'ngx-lift';
import {Component, inject, input, Signal} from '@angular/core';
import {HttpClient} from '@angular/common/http';
import {AsyncState} from 'ngx-lift';

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

  // AsyncState includes status field for granular state tracking
  userState: Signal<AsyncState<User>> = computedAsync(
    () => this.http.get<User>(`https://localhost/api/users/${this.userId()}`).pipe(createAsyncState()),
    {requireSync: true},
  );

  // Access state via:
  // userState().status  // 'idle' | 'loading' | 'reloading' | 'resolved' | 'error'
  // userState().isLoading
  // userState().error
  // userState().data
}

Example 7: Behaviors (switch, merge, concat, exhaust)

By default, computedAsync employs the switch behavior, which entails that if the computation is re-triggered before the prior one concludes, the former will be canceled. Should you wish to alter this behavior, you can provide the behavior option within the second argument options object.

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

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

  user: Signal<User | undefined> = computedAsync(
    () => this.http.get<User>(`https://localhost/api/users/${this.userId()}`),
    {behavior: 'merge'}, // or 'switch', 'concat', 'exhaust'
  );
}

switch (default)

When desiring to cancel the prior computation, the switch behavior is utilized, which is the default. If a new computation is triggered before the previous one concludes, the former will be terminated.

merge

To retain the preceding computation, the merge behavior is employed. If a new computation is initiated before the previous one is finished, the former is preserved while the latter is started.

concat

If preservation of the prior computation is desired along with waiting for its completion before initiating the new one, the concat behavior is chosen.

exhaust

In instances where disregarding the new computation while the prior one remains ongoing is necessary, the exhaust behavior is selected.

Example 8: Use with Previous Computed Value

Should there be a necessity to utilize the previously computed value in the subsequent computation, it can be accessed within the callback function as the first argument.

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

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

  user: Signal<User | undefined> = computedAsync(
    (previousValue) => {
      // Use previousValue here if you need
      return this.http.get<User>(`https://localhost/api/users/${this.userId()}`);
    },
  );
}

Example 9: Load Data Initially

Load users initially when the component loads:

thumbnail
Name
Castelino da Paz
Gender
male
thumbnail
Name
Karla Colón
Gender
female
thumbnail
Name
هستی رضاییان
Gender
female
thumbnail
Name
Filippa Thomsen
Gender
female
thumbnail
Name
Sara Hansen
Gender
female
thumbnail
Name
Mohammad Leclerc
Gender
male
thumbnail
Name
Byron Morris
Gender
male
thumbnail
Name
یلدا صدر
Gender
female
thumbnail
Name
علی رضا كامياران
Gender
male
<div>
  <button class="btn btn-primary" (click)="refresh()" [clrLoading]="usersState().isLoading">Refresh</button>
</div>

@if (usersState().isLoading) {
  <cll-spinner />
}

@if (usersState().error; as error) {
  <cll-alert [error]="error" />
}

@if (usersState().data?.results; as users) {
  <div class="card-grid">
    @for (user of users; track user.id.value) {
      <app-user-card [user]="user" />
    }
  </div>
}
import {AsyncState, computedAsync, createAsyncState, createTrigger} from 'ngx-lift';
import {Component, inject, Signal} from '@angular/core';
import {UserService} from './user.service';

export class UserListComponent {
  private userService = inject(UserService);
  private refreshTrigger = createTrigger();

  // user list will initially be fetched
  usersState: Signal<AsyncState<PaginationResponse<User>>> = computedAsync(
    () => {
      this.refreshTrigger.value();

      return this.userService.getUsers({results: 9}).pipe(createAsyncState());
    },
    {requireSync: true},
  );

  refresh() {
    this.refreshTrigger.next();
  }
}

Example 10: Load Data When Button Clicks

Load users only when a button is clicked:

<div>
  <button class="btn btn-primary" (click)="load()" [clrLoading]="deferredUsersState()?.isLoading">
    Load Users
  </button>
</div>

@if (deferredUsersState()?.isLoading) {
  <cll-spinner />
}

@if (deferredUsersState()?.error; as error) {
  <cll-alert [error]="error" />
}

@if (deferredUsersState()?.data?.results; as users) {
  <div class="card-grid">
    @for (user of users; track user.id.value) {
      <app-user-card [user]="user" />
    }
  </div>
}
import {AsyncState, computedAsync, createAsyncState, createTrigger} from 'ngx-lift';
import {Component, inject, Signal} from '@angular/core';
import {UserService} from './user.service';

export class UserListComponent {
  private userService = inject(UserService);
  private fetchTrigger = createTrigger();

  // user list will be fetched only when button clicks
  deferredUsersState: Signal<AsyncState<PaginationResponse<User>> | undefined> = computedAsync(() => {
    return this.fetchTrigger.value() ? this.userService.getUsers({results: 9}).pipe(createAsyncState()) : undefined;
  });

  load() {
    this.fetchTrigger.next();
  }
}

Example 11: Error Handling with onError

Use the onError callback to handle errors gracefully and provide fallback values. When onError returns a value, the signal will emit that value instead of throwing an error:

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

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

  // Handle errors and provide fallback value
  user: Signal<User | undefined> = computedAsync(
    () => this.http.get<User>(`https://localhost/api/users/${this.userId()}`),
    {
      onError: (error) => {
        console.error('Failed to load user:', error);
        // Return fallback user
        return {id: 0, name: 'Guest User', email: 'guest@example.com'} as User;
      }
    }
  );
}

Example 12: Error Handling with throwOnError

Use throwOnError: true to propagate errors up instead of silently handling them. This is useful when you want errors to bubble up to a global error handler:

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

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

  // Throw errors instead of silently handling them
  user: Signal<User | undefined> = computedAsync(
    () => this.http.get<User>(`https://localhost/api/users/${this.userId()}`),
    {
      throwOnError: true,  // Errors will propagate up
      onError: (error) => {
        console.error('API Error:', error);
        // Can still log/handle but error will still throw
        return undefined;  // Won't be used since error throws
      }
    }
  );
}

API Reference

computedAsync

Computes values from Promises, Observables, or regular values, returning a Signal that emits the computed value.

Signature

computedAsync<T>(
  computation: (previousValue?: T) => Promise<T> | Observable<T> | T,
  options?: ComputedAsyncOptions<T>
): Signal<T | undefined> | Signal<T>

interface ComputedAsyncOptions<T> {
  initialValue?: T;
  requireSync?: boolean;
  behavior?: 'switch' | 'merge' | 'concat' | 'exhaust';
  onError?: (error: unknown) => T | undefined;
  throwOnError?: boolean;
}

Parameters

  • computation: (previousValue?: T) => Promise<T> | Observable<T> | T

    A function that returns a Promise, Observable, or regular value. The function receives the previous computed value as an optional first argument.

  • options?: ComputedAsyncOptions<T>

    Configuration options:

    • initialValue?: T - The initial value for the computed signal before the computation completes.
    • requireSync?: boolean - If true, requires the computation to emit synchronously. This ensures the signal type excludes undefined.
    • behavior?: 'switch' | 'merge' | 'concat' | 'exhaust' - The flattening strategy for handling multiple computations. Defaults to 'switch'.
    • onError?: (error: unknown) => T | undefined - New! Error handler that receives errors from async operations. Can return a fallback value to recover from errors.
    • throwOnError?: boolean - New! If true, errors will be thrown and propagate up. If false (default), errors can be handled by onError.

Returns

A Signal that emits the computed value. The signal type depends on the requireSync option: Signal<T | undefined> if requireSync is false or omitted, Signal<T> if requireSync is true.

Behaviors

  • switch (default): Cancels the previous computation when a new one is triggered.
  • merge: Preserves the previous computation while starting a new one when triggered.
  • concat: Waits for the previous computation to complete before starting a new one.
  • exhaust: Ignores new computations while a previous one is still ongoing.