Application loading indicator using NgRX and Angular.
Why?
Most applications perform asynchronous loading of data, and it may be necessary for your application to show a loading indicator while the data is being loaded. Using NgRX we can easily toggle the display of a loading indicator or spinner based on the actions that are dispatched to the store.
Sample Application
I’ll be using two branches from a sample app that is a tour of heroes demo application:
- ngrx-refactor-2 is the first take
- ngrx-refactor-3 is the second take
You can download or clone the ngrx-refactor-2 branch of the repository at:
First Take
First, let’s take a look at an example State
interface that includes a loading
property.
You can see the first take working in the example application’s ngrx-refactor-2 branch:
$ git checkout ngrx-refactor-2
$ cd server && yarn
Here is the trimmed contents of src/app/state/powers/reducers/powers.ts:
export interface State extends EntityState<Power> {
loading: boolean;
}
export const adapter: EntityAdapter<Power> = createEntityAdapter();
const initialState: State = adapter.getInitialState({
loading: false
});
export function reducer(state: State = initialState, action: PowersAction) {
switch (action.type) {
case LOAD_POWERS:
return {...state, loading: true};
case LOAD_POWERS_SUCCESS:
state = {...state, loading: false};
return adapter.addAll(action.payload, state);
default:
return state;
}
}
export const isLoading = (state: State) => state.loading;
A few things to note:
- First, we defined a
loading
property in ourState
interface. - Second, we set the value to
false
in theinitialState
. - In our
reducer()
function we toggle the value.
We then create a selector for the loading property in the src/app/state/powers/reducers/index.ts file:
export const isPowerLoading = createSelector(
getPowersEntityState,
fromPowers.isLoading,
);
In our stateful (container) component we can select this value from the store using the selector. ThiS example is from the src/app/+powers/containers/index/index.component.ts file:
loading: Observable<boolean>;
ngOnInit() {
this.loading = this.store.pipe(select(isPowerLoading));
}
In the stateless (dumb) component we can then use this value to toggle the display of Material’s spinner component. Here is the input into the component in src/app/+powers/components/powers/powers.component.ts:
export class PowersComponent {
@Input() loading: boolean;
}
And here is the template in src/app/+powers/components/powers/powers.component.html:
<mat-list *ngIf="!loading; else spinner">
<!-- list the powers -->
</mat-list>
<ng-template #spinner>
<div fxLayout="row" fxLayoutAlign="center center">
<mat-spinner></mat-spinner>
</div>
</ng-template>
In the template code above we use the NgIf
directive to toggle the display of the spinner based on the loading
value.
If loading is false then we display the list of powers, otherwise, we display the <ng-template>
with the spinner in the center.
Second Take
While the first take is a good approach to using NgRX to toggle the display of a loading indicator or spinner, we can refactor this to better use NgRX, specifically, the effects that are triggered by actions in our application.
The solution for the second take is available on the sample application’s ngrx-refactor-3 branch:
$ git checkout ngrx-refactor-3
You can download or clone the ngrx-refactor-3 branch via:
Actions
To get started, let’s create two actions:
ShowSpinner
, which will show the spinner.HideSpinner
, which will hide the spinner.
Let’s take a look at the src/app/state/shared/actions/spinner.ts file:
import { Action } from "@ngrx/store";
import { createActionType } from "../utils";
export const SPINNER_SHOW = createActionType("SPINNER_SHOW");
export const SPINNER_HIDE = createActionType("SPINNER_HIDE");
export class HideSpinner implements Action {
readonly type = SPINNER_HIDE;
}
export class ShowSpinner implements Action {
readonly type = SPINNER_SHOW;
}
export type SpinnerAction = ShowSpinner | HideSpinner;
To summarize:
- We define two action type constants:
SPINNER_SHOW
andSPINNER_HIDE
. - We then define two action classes that implement the
Action
interface:HideSpinner
andShowSpinner
. - Finally, we export the
SpinnerAction
type that is a union of both theShowSpinner
andHideSpinner
types.
Reducer
Next, we’ll define a reducer()
function to mutate the state of our application.
Let’s take a look at src/app/state/shared/reducers/spinner.ts:
import { SPINNER_HIDE, SPINNER_SHOW, SpinnerAction } from "../actions/spinner";
export interface State {
show: boolean;
}
const initialState: State = {
show: false
};
export function reducer(state: State = initialState, action: SpinnerAction) {
switch (action.type) {
case SPINNER_HIDE:
return { ...state, show: false };
case SPINNER_SHOW:
return { ...state, show: true };
default:
return state;
}
}
export const isShowing = (state: State) => state.show;
Let’s quickly review:
- First, we define a
State
interface with a singleshow
property that is aboolean
value. - Next, we set the
initialState
object with the value ofshow
set tofalse
. - In our
reducer()
function we will return a new copy of the state with the value ofshow
set totrue
orfalse
.
Don’t forget to add the new feature state to your application’s State
.
This is located in the src/app/core/state/app.reducer.ts file in the example application:
export const appReducer: ActionReducerMap<AppState> = {
router: routerReducer,
snackbar: fromSnackbar.reducer,
spinner: fromSpinner.reducer
};
I’ve simply added the spinner
property with a reference to the reducer()
function we just created and exported.
Selector
If you noticed, I defined an isShowing
function that accepts the State
object and returns the boolean show
value in the src/app/state/shared/reducers/spinner.ts file.
Using this function we’ll create a new selector in src/app/shared/reducers/index.ts:
import { createSelector, createFeatureSelector } from '@ngrx/store';
import * as fromSpinner from './spinner';
export const selectSpinnerEntity =
createFeatureSelector < fromSpinner.State > 'spinner';
export const isSpinnerShowing = createSelector(
selectSpinnerEntity,
fromSpinner.isShowing,
);
We’ll be using the isSpinnerShowing
selector in our application to toggle the display of the loading indicator or spinner.
Effects
With our actions created and our reducer function to mutate the state of our app, we are ready to implement the actions in our effects. In this example, I am only going to update the effects for the powers module. You’ll need to implement the effects in each entity where you are planning to toggle the loading indicator.
Let’s take a look at src/app/state/powers/effects/powers.ts:
type showSpinnerTypes =
| AddPower
| DeletePower
| LoadPower
| LoadPowers
| UpdatePower;
const showSpinnerActions = [
ADD_POWER,
DELETE_POWER,
LOAD_POWER,
LOAD_POWERS,
UPDATE_POWER
];
type hideSpinnerTypes =
| AddPowerSuccess
| DeletePowerSuccess
| LoadPowersSuccess
| LoadPowersSuccess
| UpdatePowerSuccess;
const hideSpinnerActions = [
ADD_POWER_SUCCESS,
DELETE_POWER_SUCCESS,
LOAD_POWER_SUCCESS,
LOAD_POWERS_SUCCESS,
UPDATE_POWER_SUCCESS
];
@Injectable()
export class PowersEffects {
@Effect()
showSpinner: Observable<Action> = this.actions
.ofType<showSpinnerTypes>(...showSpinnerActions)
.pipe(map(() => new ShowSpinner()));
@Effect()
hideSpinner: Observable<Action> = this.actions
.ofType<hideSpinnerTypes>(...hideSpinnerActions)
.pipe(map(() => new HideSpinner()));
}
Let’s review the code above:
- First, I’ve omitted the import statements. You can check out the full example code if you need to reference them.
- We then create a set of types and actions for when we want to show or hide the spinner.
- The
showSpinnerTypes
is a union of all actions for when we will show the spinner. - The
showSpinnerActions
is an array of the action types for when we will show the spinner. - The
hideSpinnerTypes
is a union of all actions for when we will hide the spinner. For the sake of brevity, I am only hiding the spinner when the action is successful. This is obviously not real-world. You’ll want to also include any failure/error actions. - The
hideSpinnerActions
is an array of the action types for when we will hide the spinner. - We then define two properties:
showSpinner
andhideSpinner
in thePowersEffects
class. - The
showSpinner
property will be triggered when any of theshowSpinnerActions
are dispatched to the store, and we will dispatch theShowSpinner
action as a result. - The
hideSpinner
property will be triggered when any of thehideSpinnerActions
are dispatched to the store, and we will dispatch theHideSpinner
action as a result.
Template
I’m going to add the <mat-spinner>
component to the LayoutComponent
that I use to wrap my container templates.
Where you implement the actual loading indicator or spinner will likely vary based on your application’s architecture.
You may also be using a different component from another library or a custom loading indicator component.
The goal is the same, toggle the display of the loadig indicator based on the isSpinnerShowing
selector.
Let’s look at the example in the src/app/shared/components/layout/layout.component.ts file:
export class LayoutComponent implements OnInit {
loading: Observable<boolean>;
constructor(private store: Store<AppState>) {}
ngOnInit() {
this.loading = this.store.pipe(select(isSpinnerShowing));
}
}
We simply use the select()
function, providing the isSpinnerShowing
selector, to the store to retrieve the boolean value.
I have a loading
property that is an Observable
of a boolean
value, which we will use in the template to toggle the display of the <mat-spinner>
.
And, here’s the snippet of the template that toggles the display of the spinner from src/app/shared/components/layout/layout.component.html:
<ng-content *ngIf="!(loading | async); else spinner"></ng-content>
<ng-template #spinner>
<div fxLayout="row" fxLayoutAlign="center center">
<mat-spinner></mat-spinner>
</div>
</ng-template>
In the template we simply use the NgIf
directive to toggle the display of the <ng-content>
for the application.
When we are not loading anything, we’ll display the transcluded content here.
When we are loading, we will display the <ng-template>
that includes the <mat-spinner>
loading indicator.
Conclusion
Using NgRX we can effectively implement an application-wide method for toggling the display of a loading indicator.
While we can implement this on a per-component level, we can take advantage of the @ngrx/effects module for dispatching the ShowSpinner
and HideSpinner
actions based on the actions for adding, deleting, loading and updating an entity.