ngx-http-request-state
TypeScript icon, indicating that this package has built-in type declarations

3.1.0 • Public • Published

ngx-http-request-state

An Angular library for wrapping HttpClient responses with loading & error information.

Allows observing the whole lifecycle of HTTP requests as a single observable stream of state changes, simplifying handling of loading, loaded & error states.

If you have found this library useful, please consider donating to say thanks and support its development:

Buy Me a Coffee at ko-fi.com

Versions

Version ^3.1.0 supports Angular 14 - 17. Version 3.0.0 supports Angular 14, 15 & 16. Version ^2.1.0 supports Angular 14 & 15. Version 1.2.0 supports Angular 8 to 13. Angular versions 7 and earlier are not supported.

API

The library declares an HttpRequestState interface to reflect the state of an HTTP request:

export interface HttpRequestState<T> {
  /**
   * Whether a request is currently in-flight.  true for a "loading" state,
   * false otherwise.
   */
  readonly isLoading: boolean;
  /**
   * The response data for a "loaded" state, or optionally the last-known data
   * (if any) for a "loading" or "error" state.
   */
  readonly value?: T;
  /**
   * The response error (if any) for an "error" state.
   */
  readonly error?: HttpErrorResponse | Error;
}

There are three subtypes of HttpRequestStatus provided with more tightly-defined members, as well as a set of type-guard predicates:

export interface LoadingState<T> extends HttpRequestState<T> {
  readonly isLoading: true;
  readonly value?: T;
  readonly error: undefined;
}

export interface LoadedState<T> extends HttpRequestState<T> {
  readonly isLoading: false;
  readonly value: T;
  readonly error: undefined;
}

export interface ErrorState<T> extends HttpRequestState<T> {
  readonly isLoading: false;
  readonly value?: T;
  readonly error: HttpErrorResponse | Error;
}

export declare function isLoadingState<T>(state?: HttpRequestState<T>): state is LoadingState<T>;
export declare function isLoadedState<T>(state?: HttpRequestState<T>): state is LoadedState<T>;
export declare function isErrorState<T>(state?: HttpRequestState<T>): state is ErrorState<T>;

Finally, a function called httpRequestStates() is provided which returns an RxJs operator that transforms an Observable<HttpResponse<T>> into an Observable<HttpRequestState<T>>:

export class SomeComponent {
  /**
   * Will immediately emit a LoadingState, then either a LoadedState<MyData> or
   * an ErrorState, depending on whether the underlying HTTP request was successful.
   */
  readonly myData$: Observable<HttpRequestState<MyData>> = this.httpClient
    .get<MyData>(someUrl)
    .pipe(httpRequestStates());

  constructor(private httpClient: HttpClient) {}
}

The associated HTML template can then async-pipe this state to display either a loading spinner, the data, or an error state:

<ng-container *ngIf="myData$ | async as myData">
  <!-- Show a spinner if state is loading -->
  <my-loading-spinner *ngIf="myData.isLoading"></my-loading-spinner>

  <!-- Show the data if state is loaded -->
  <my-data-view *ngIf="myData.value" [myData]="myData.value"></my-data-view>

  <!-- Show an error message if state is error -->
  <my-error-state *ngIf="myData.error" [error]="myData.error"></my-error-state>
</ng-container>

switchMap safety

The httpRequestStates() operator catches errors and replaces them with ordinary (next) emission of an ErrorState object instead, so it will never throw an error.

This means when used inside a switchMap, no special error handling is required to prevent the outer observable being unsubscribed following an error response in the inner observable:

export class SomeComponent {
  /**
   * Every time the source observable (activatedRoute.params) emits a value,
   * this observable will immediately emit a LoadingState followed later by
   * either a LoadedState<MyData> or an ErrorState.
   *
   * It will continue to emit new HttpRequestStates following values being emitted
   * from the source observable, even if errors were thrown by the http client
   * for earlier requests.
   */
  readonly myData$: Observable<HttpRequestState<MyData>> = this.activatedRoute.params.pipe(
    pluck('id'),
    distinctUntilChanged(),
    // The .pipe(httpRequestStates()) needs to be _inside_ the switchMap so that it can
    // catch errors throw by the inner observable.  If httpRequestStates() was just
    // placed after the switchMap operator in the outer pipe instead, then an error from
    // the inner observable (httpClient.get) would make the swichMap operator unsubcribe
    // from the outer observable, and so we'd no longer react to changes in the route params.
    switchMap((id) => this.httpClient.get<MyData>(`${baseUrl}?id=${id}`).pipe(httpRequestStates()))
  );

  constructor(private httpClient: HttpClient, private activatedRoute: ActivatedRoute) {}
}

Merging

The intention of this library is to provide a view model loading state, where you prep all the data first then, pipe it through httpRequestStates towards the end, instead of piping it at the lowest level, for instance in an API service.

An example of this can be seen in the examples app.

If you however already have multiple HttpRequestState<T> objects and would like to merge the values together, then mergeStates can be used.

Consider the following example where we assume we have no control over MyDataService and it already wraps requests in HttpRequestState:

// Third party
@Injectable()
export class MyDataService {
  constructor(private httpClient: HttpClient) {}

  getMyData(someParameter: any) {
    return this.httpClient.get<MyData>(someUrl + someParameter).pipe(httpRequestStates());
  }
}

// Our component
export class SomeComponent {
  readonly myDataCollection$ = combineLatest([
    this.myDataService.getMyData('red'),
    this.myDataService.getMyData('blue'),
  ]).pipe(
    map((states) =>
      mergeStates(states, (dataArray) => {
        // Merge list of data together then return a new instance of MyData
      })
    )
  );

  constructor(private myDataService: MyDataService) {}
}

(We use combineLatest instead of forkJoin to get loading updates)

Using mergeStates allows you to act "inside" the HttpRequestState, directly on the values or the errors.

As long as one of the states are loading, the resulting merged state will be a LoadingState. When all finish successfully, the callback of the second argument is called with all the available values.

If an error occurs in any of the requests, the merged state will be an ErrorState. By default the first of the errors will be returned. It is possible to override this with the third argument.

Example:

export class SomeComponent {
  readonly myDataCollection$ = combineLatest([
    this.myDataService.getMyData('will-fail'),
    this.myDataService.getMyData('will-also-fail'),
    this.myDataService.getMyData('blue'),
  ]).pipe(
    map((states) =>
      mergeStates(
        states,
        (dataArray) => {
          // Merge list of data together then return a new instance of MyData
        },
        (errors) => {
          // Combine the errors and return a new instance of HttpErrorResponse or Error
        }
      )
    )
  );

  constructor(private myDataService: MyDataService) {}
}

Examples

See the examples app for more example use cases.

Versions

Current Tags

  • Version
    Downloads (Last 7 Days)
    • Tag
  • 3.1.0
    158
    • latest

Version History

Package Sidebar

Install

npm i ngx-http-request-state

Weekly Downloads

424

Version

3.1.0

License

MIT

Unpacked Size

45.3 kB

Total Files

18

Last publish

Collaborators

  • daiscog