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

- Name
- Lester Green
- Gender
- male

- Name
- Bradley Adams
- Gender
- male

- Name
- Samira Nissen
- Gender
- female

- Name
- George Wright
- Gender
- male

- Name
- Tanish Nagane
- Gender
- male

- Name
- Arnold Caldwell
- Gender
- male

- Name
- Alexandros Hoogewerf
- Gender
- male

- Name
- Stanoje Dinčić
- Gender
- male

- 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();
}
}
createTrigger
here. 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();
}
}