createAsyncState

The createAsyncState operator transforms an observable of type T into an observable of AsyncState<T>, encapsulating loading, error, and data states in a standardized structure. This eliminates the need to manage separate variables for loading states, errors, and data, making your codebase more organized and maintainable.

The AsyncState interface structure that the operator returns:

export interface AsyncState<T, E = HttpErrorResponse> {
  status: ResourceStatus;  // 'idle' | 'loading' | 'reloading' | 'resolved' | 'error'
  isLoading: boolean;
  error: E | null;
  data: T | null;
}

type ResourceStatus = 'idle' | 'loading' | 'reloading' | 'resolved' | 'error';

// For non-HTTP errors, you can override the default:
// AsyncState<Product[], Error>

Examples

Example 1: Simple Request

Handle a single API request with loading and error states:

import {createAsyncState} from 'ngx-lift';
// ... other imports

@Component({
  template: `
    <ng-container *ngIf="usersState$ | async as usersState">
      <cll-spinner *ngIf="usersState.isLoading"></cll-spinner>

      <cll-alert *ngIf="usersState.error as error" [error]="error"></cll-alert>

      <div class="card-grid" *ngIf="usersState.data as users">
        <app-user-card *ngFor="let user of users" [user]="user"></app-user-card>
      </div>
    </ng-container>
  `,
})
export class UserCardListComponent {
  usersState$ = inject(UserService).getUsers({results: 9}).pipe(createAsyncState());
}
thumbnail
Name
Valtteri Halko
Gender
male
thumbnail
Name
Mihaela Vrhovac
Gender
female
thumbnail
Name
Mona Lambert
Gender
female
thumbnail
Name
Pascual Molina
Gender
male
thumbnail
Name
Gerlinda Jak
Gender
female
thumbnail
Name
Lucy Lemoine
Gender
female
thumbnail
Name
آدرین کوتی
Gender
male
thumbnail
Name
Sila Slobodyan
Gender
male
thumbnail
Name
Laurent Bertrand
Gender
male

Example 2: With Callbacks

Add side effects for success and error scenarios:

import {createAsyncState} from 'ngx-lift';

this.userService.getUsers().pipe(
  createAsyncState({
    next: (res) => console.log(res), // success callback
    error: (error) => console.error(error), // error callback
  }),
).subscribe();

Example 3: Providing initialValue

In scenarios where you have initial data (e.g., from route state), you can provide an initial value to avoid showing a loading spinner. This is useful when navigating from a list to a detail page where you already have some data:

import {createAsyncState} from 'ngx-lift';
import { Location } from '@angular/common';
import {noop} from 'rxjs';

import {User} from '../../models/user.model';

@Component({
  selector: 'app-user-detail',
  imports: [SpinnerComponent, AlertComponent],
  templateUrl: './user-detail.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UserDetailComponent {
  private userService = inject(UserService);
  private location = inject(Location);

  userState$ = this.userService
    .getUserById(1)
    .pipe(createAsyncState<User>(noop, {status: 'idle', isLoading: false, error: null, data: this.location.getState()}));
}

Example 4: Dependent Requests

Chain multiple requests using switchMap:

import {createAsyncState} from 'ngx-lift';

data$ = firstCall$.pipe(
  switchMap(() => this.shopService.products$),
  createAsyncState()
);

Example 5: Template Usage

Use the async pipe in templates to handle different states:

import {createAsyncState} from 'ngx-lift';

// In component:
userState$ = this.userService.getUser(id).pipe(createAsyncState());

// In template (using isLoading - recommended):
@if (userState$ | async; as state) {
  @if (state.isLoading) {
    <cll-spinner />
  }
  @if (state.error) {
    <cll-alert [error]="state.error" />
  }
  @if (state.data; as user) {
    <user-card [user]="user" />
  }
}

// Alternative: using status for granular state tracking:
@if (userState$ | async; as state) {
  @if (state.status === 'loading') {
    <p>Initial load...</p>
  }
  @if (state.status === 'reloading') {
    <p>Refreshing...</p>
  }
  @if (state.status === 'error' && state.error) {
    <cll-alert [error]="state.error" />
  }
  @if (state.status === 'resolved' && state.data; as user) {
    <user-card [user]="user" />
  }
}

API Reference

createAsyncState

Transforms an Observable of type T into an Observable of AsyncState<T, E>.

Signature

createAsyncState<T, E>(
  observerOrNextForOrigin?: Partial<TapObserver<T>> | ((value: T) => void),
  initialValue?: AsyncState<T, E>
): UnaryFunction<Observable<T>, Observable<AsyncState<T, E>>>

Parameters

  • observerOrNextForOrigin

    (Optional) Callback function or observer object for handling side effects when the observable emits values or errors. Accepts either a partial TapObserver<T> object or a simple function (value: T) => void.

    This parameter has the same type signature as RxJS tap operator, allowing you to perform side effects like logging, caching, or triggering other actions.

  • initialValue

    (Optional) The initial AsyncState value to use before the observable emits its first value.

    Defaults to { status: 'loading', isLoading: true, error: null, data: null }. Useful when you have pre-existing data (e.g., from route state) and want to avoid showing a loading spinner initially.

Returns

A function that transforms an observable stream into an asynchronous state.

AsyncState Interface

The AsyncState object encapsulates the following properties:

  • status: ResourceStatus

    New! Granular status tracking: 'idle' | 'loading' | 'reloading' | 'resolved' | 'error'

  • isLoading: boolean

    Recommended! Indicates if any loading operation is in progress. Returns true when status === 'loading' || status === 'reloading'.

  • error: E | null

    Represents any encountered errors during the operation.

  • data: T | null

    Holds the successful result of the operation.