computedAsync

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

Usage

To utilize computedAsync, provide a function returning a Promise, Observable, or regular value. It then returns a Signal emitting the computed value.

Works with Promises

import {computedAsync} from 'ngx-lift';

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

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

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.

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()}`),
  );
}

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.

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

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.

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

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

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

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 },
  );
}

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

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

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

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.

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

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

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.

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.

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

  user = computedAsync(
    (previousValue) => {
      // Use previousValue here if you need

      return this.http.get<User>(`https://localhost/api/users/${this.userId()}`);
    },
  );
}

Load data when a button clicks

Load users initially

thumbnail
Name
Lester Green
Gender
male
thumbnail
Name
Bradley Adams
Gender
male
thumbnail
Name
Samira Nissen
Gender
female
thumbnail
Name
George Wright
Gender
male
thumbnail
Name
Tanish Nagane
Gender
male
thumbnail
Name
Arnold Caldwell
Gender
male
thumbnail
Name
Alexandros Hoogewerf
Gender
male
thumbnail
Name
Stanoje Dinčić
Gender
male
thumbnail
Name
Sienna Ervik
Gender
female
<div>
  <button class="btn btn-primary" (click)="refresh()" [clrLoading]="usersState().loading === true">Refresh</button>
</div>

@if (usersState().loading) {
  <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';

export class UserDetailComponent {
  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();
  }
}

Load users when button clicks

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

@if (deferredUsersState()?.loading) {
  <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';

export class UserDetailComponent {
  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();
  }
}