Handle API call state NICELY

Siyang Kern Zhao
Angular In Depth
Published in
6 min readDec 23, 2018

AngularInDepth is moving away from Medium. More recent articles are hosted on the new platform inDepth.dev. Thanks for being part of indepth movement!

In this article, I will introduce a way of handling state for API calls and reducing boilerplate by abstracting common logic. It’s powerful, clean, less error-prone. This article assumes we are using NgRx to manage state.

Photo by Sven Fischer on Unsplash

I bet API calls are one of the most common requirements for web development. Many apps have tons of API calls. In terms of user experience, it’s always a good practice to indicate the status of an API call, such as showing a loading spinner or error message. I have seen many ways of modelling the state of an API call and found one main pain point — heavy boilerplate, which usually causes further problems.

Heavy boilerplate

For example, assume the following business requirement:

  1. Send an API request to get a list of today’s news.
  2. Show a spinner while loading
  3. Show the loaded list of news on success.

Many developers would design a state model in the following way along with two actions (Let’s say LoadNews and LoadNewsSuccess and two reducer cases to change the loading and entities state.

export interface News {
loading: boolean;
entities: string[];
}

So far we don’t see any problems here. It’s very “standard”.

Let’s say we have 20 (or even more) API requests in this app. Now come the problems:

  1. Lots of boilerplate. We need to have the API state (loading) 20 times, 40 actions, and implement 40 reducer cases. That’s a lot of code with repetitive logic.
  2. Inconsistent naming. Let’s say the 20 API calls are implemented by 4 developers. They may have different naming conventions. For example, the loading could be isLoading , waiting , isWaiting , started etc.

Actually the above API state model only has one state loading . However, a complete API state is supposed to have more states (to be discussed in the next section), which will make the previous 2 points even worse.

Let’s solve this problem NICELY.

What is the complete state set?

A complete API call cycle could have the following states:

  1. API call not started
  2. API call started but no response yet
  3. API call received a successful response
  4. API call received an error response

So we can design a general model like this (let’s call it Loadable):

export interface Loadable {
loading: boolean;
success: boolean;
error: any;
}

It’s easy to map the 4 states to the value of the 3 fields.

I would also create 4 straight-forward helper functions to update the loadable state. Note they are pure functions and return new loadable objects:

export function createDefaultLoadable() {
loading: false,
success: false,
error: null,
}
export function onLoadableLoad(loadable) {
return {
...loadable,
loading: true,
success: false,
error: null,
};
}
export function onLoadableSuccess(loadable) {
return {
...loadable,
loading: false,
success: true,
error: null,
};
}
export function onLoadableError(loadable, error) {
return {
...loadable,
loading: false,
success: false,
error: error,
};
}

Apply loadable to our example of loading news list

Model

Apart from the 3 fields of loadable, we need one more state to store the list of news we got from the API. So, we can have the following model:

export interface News extends Loadable {
news: string[];
}
export function createDefaultNews(): News {
return {
...createDefaultLoadable(),
entities: []
};
}

Actions

Actions remain the same as the ngrx convention.

export enum NewsActionsTypes {
Load = '[NEWS PAGE] LOAD NEWS',
LoadSuccess = '[NEWS PAGE] LOAD NEWS SUCCESS',
LoadError = '[NEWS PAGE] LOAD NEWS ERROR',
}

export class LoadNews implements Action {
readonly type = NewsActionsTypes.Load;
}

export class LoadNewsSuccess implements Action {
readonly type = NewsActionsTypes.LoadSuccess;
constructor(public payload: {entities: string[]}) {}
}

export class LoadNewsError implements Action {
readonly type = NewsActionsTypes.LoadError;
constructor(public error: any) {}
}
export type NewsActions = LoadNews | LoadNewsSuccess | LoadNewsError

Reducer (Not the best yet)

We use a reducer to change the state according to the 3 different actions.

export function newsReducer(state: News = createDefaultNews(), action: NewsActions): News {
switch (action.type) {
case NewsActionsTypes.Load:
return onLoadableLoad(state);
case NewsActionsTypes.LoadSuccess:
return {
...onLoadableSuccess(state),
entities: action.payload.entities
};
case NewsActionsTypes.LoadError:
return onLoadableError(state, action.error);
default:
return state;
}
}

Effects

@Effect()
loadNews = this.actions$.pipe(
ofType(NewsActionsTypes.Load),
switchMap(action => {
return this.http.get('some url').pipe(
map((response: any) => new LoadNewsSuccess({entities: response.todaysNews})),
catchError(error => of(new LoadNewsError(error)))
);
}),
);

UI Component

@Component({
selector: 'app-news',
template: `
<button (click)="load()">Load News</button>

<!--loading spinner-->
<p *ngIf="(news$ | async).loading">loading...</p>

<p *ngFor="let item of (news$ | async).entities">{{item}}</p>
`
})
export class NewsComponent {

news$: Observable<News>;

constructor(private store: Store<{news: News}>) {
this.news$ = this.store.select(state => state.news);
}

load() {
this.store.dispatch(new LoadNews());
}
}

This is enough to make it work. However, this only helps with enforcing consistent naming by extending loadable, and helps with making sure the state changes correctly by using the helper functions. It doesn’t really reduce the boilerplate. Imagine if we have 20 API calls, we still need to handle every action (load, loadSuccess, loadError) in each one of the 20 reducers. And 20 of them share the same logic of changing the states. (i.e. loading success error )

Abstract API state change logic from reducer

Let’s define a higher order function withLoadable with takes in a reducer and three action type strings as parameters and returns a new enhanced reducer.

export function withLoadable(baseReducer, {loadingActionType, successActionType, errorActionType}) {
return (state, action) => {
if (action.type === loadingActionType) {
state = onLoadableLoad(state);
}
if (action.type === successActionType) {
state = onLoadableSuccess(state);
}
if (action.type === errorActionType) {
state = onLoadableError(state, action.error);
}
return baseReducer(state, action);
};
}

In this way, the news reducer can be like this:

// base reducer should only update non-loadable states
function baseNewsReducer(state: News = createDefaultNews(), action: NewsActions): News {
switch (action.type) {
case NewsActionsTypes.LoadSuccess:
return {
...state,
entities: action.payload.entities
};
default:
return state;
}
}
// withLoadable enhances baseReducer to update loadable state
export function newsReducer(state: News, action: NewsActions): News {
return withLoadable(baseNewsReducer, {
loadingActionType: NewsActionsTypes.Load,
successActionType: NewsActionsTypes.LoadSuccess,
errorActionType: NewsActionsTypes.LoadError,
})(state, action);
}

baseNewsReducer handles non-loadable states (i.e. entities)

newsReducer will actually apply withLoadable enhancer to baseReducer to give some “magic” to baseReducer , and the “magic” is to handle the loadable state changes automatically.

In this way, if we have 20 API calls and want to store all of the 20 * 3 = 60 states, we can just apply withLoadable to the 20 base reducers. In the 20 base reducers we don’t care how the loadable state should be updated. So it saves us a lot of work to manually update the API state.

Bonus: Connecting Loadable with the UI Component

Loadable actually provides a really consistent contract so that it could be seamlessly connected with a global UI component. For example, I can create a common component loading-container to handle the loading UI, error UI globally. And the only contract to the outside world is just a @Input of Loadable

@Component({
selector: 'loading-container',
template: `
<div *ngIf="loadable.loading">This is loading spinner...</div>
<div *ngIf="loadable.error">{{loadable?.error?.message || 'Something went wrong'}}</div>
<ng-container *ngIf="loadable.success">
<ng-content></ng-content>
</ng-container>
`
})
export class LoadingContainerComponent {
@Input() loadable: Loadable;
}

This will allow us to handle every spinner/error of an API call just by using this loading-container component like this, which also saves lots of boilerplate.

<loading-container [loadable]="news$ | async">
<p *ngFor="let item of (news$ | async).entities">{{item}}</p>
</loading-container>

Please find the final code in StackBlitz, or the Github Repo. The only difference is that it is strictly typed to have better coding experience in real life. Also, it use mock API call to get a list of news.

If you want to use it in your project, I have an npm package there for you. Check it out here.

Happy coding!

--

--