Angular library for wrapping HttpClient responses with loading & error information to allow observing load/loading/error state changes as a stream of events.
npm install ngx-http-request-stateAn 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:
Version ^3.6.0 supports Angular 14 - 20.
Version ^3.5.0 supports Angular 14 - 19.
Version ^3.2.0 supports Angular 14 - 18.
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.
The library declares an HttpRequestState interface to reflect the state of an HTTP
request:
``typescript`
export interface HttpRequestState
/**
* 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:
`typescript
export interface LoadingState
readonly isLoading: true;
readonly value?: T;
readonly error: undefined;
}
export interface LoadedState
readonly isLoading: false;
readonly value: T;
readonly error: undefined;
}
export interface ErrorState
readonly isLoading: false;
readonly value?: T;
readonly error: HttpErrorResponse | Error;
}
export declare function isLoadingState
export declare function isLoadedState
export declare function isErrorState
`
Finally, a function called httpRequestStates() is provided which returns an RxJs operatorObservable
that transforms an into an Observable:
`typescript
export class SomeComponent {
/**
* Will immediately emit a LoadingState, then either a LoadedState
* an ErrorState, depending on whether the underlying HTTP request was successful.
*/
readonly myData$: Observable
.get
.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:
`html
`
The httpRequestStates() operator catches errors and replaces them with ordinary (next) emission ofErrorState
an 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:
`typescript${baseUrl}?id=${id}
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
*
* 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
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).pipe(httpRequestStates()))
);
constructor(private httpClient: HttpClient, private activatedRoute: ActivatedRoute) {}
}
`
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 objects and would like to merge the values together,mergeStates
then can be used.
Consider the following example where we assume we have no control over MyDataService and it already wraps requests in HttpRequestState:
`typescript
// Third party
@Injectable()
export class MyDataService {
constructor(private httpClient: HttpClient) {}
getMyData(someParameter: any) {
return this.httpClient.get
}
}
// 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:
`typescript
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) {}
}
``
See the examples app for more example use cases.