Internalize Request Status to API Requests in Angular with RxJS
Monday, November 16, 2020
Internalize Request Status to API Requests in Angular with RxJS
Most of the times, front-end developers need to display a loading
interface while an API request is pending
(aka loading
). Depending on the current project's State Management strategy that developers are using for their projects, there are different approaches to keep track of this loading
state: a loading
field on the Component, a loading
state in the Store etc... What these approaches have in common is that the loading
state is kept externally from the API Request itself.
Internalizing loading
state into the API Request can allow for incremental UI which means to display blocks of UI for a specific page incrementally as the API Request for that block is completed. With RxJS, this task is trivial, and type-safe.
Code Examples
to-api-response.operator.ts
/**
* Arbitrary states of an API request
*/
export enum ApiResponseStatus {
Loading = "loading",
Success = "success",
Failure = "failure"
}
export interface ApiResponse<TData> {
data: TData;
status: ApiResponseStatus;
error: unknown;
}
/**
* A transformation operation to trasnform Observable<TData> to Observable<ApiResponse<TData>>
*
* - startsWith() to kick-off an emission with the `initialValue` and the status of `loading`.
* - map() to map the actual data from the API and switch status to `success`
* - catchError() to handle error cases with errObsFactoryOrRethrow
* - Just rethrow the error without transforming: pass in true
*
*
* ```ts
* timer(5000).pipe(mapTo('response'), toApiResponse(''));
* --> {status: 'loading', data: '', error: ''}
* --> 5 seconds
* --> {status: 'success', data: 'response', error: ''}
* ```
*/
export function toApiResponse<TData>(
initialValue: TData extends object ? Partial<TData> : TData,
errObsFactoryOrRethrow?:
| true
| ((err: unknown) => unknown | Observable<unknown>)
): UnaryFunction<Observable<TData>, Observable<ApiResponse<TData>>> {
return pipe(
map<TData, ApiResponse<TData>>(data => ({
status: ApiResponseStatus.Success,
data,
error: ""
})),
startWith({
status: ApiResponseStatus.Loading,
data: initialValue as TData,
error: ""
}),
catchError(err => {
const defaultFailureResponse = {
status: ApiResponseStatus.Failure,
data: initialValue as TData
};
if (errObsFactoryOrRethrow == null) {
return of<ApiResponse<TData>>({
...defaultFailureResponse,
error: err.message || err.error || err.toString()
});
}
if (typeof errObsFactoryOrRethrow === "function") {
const error = errObsFactoryOrRethrow(err);
if (isObservable(error)) {
return error.pipe(
map<unknown, ApiResponse<TData>>(e => ({
...defaultFailureResponse,
error: e
}))
);
}
return of<ApiResponse<TData>>({
...defaultFailureResponse,
error
});
}
return throwError(err);
})
);
}
Have a question or comment?