Aug 5th, 2017 - written by Kimserey with .
Last month, I describe a way to manage global state with ngrx/store. With the store, we mamage the overal state of the Angular application in a single global object. Loading and retrieving data affects a single main state object. This simplication gives opportunities to other simplications. Like for example, if we loaded once a collection of items, we wouldn’t need to reload it when a component is displayed as it is available in the state. But how can we ensure that and more importantly how can we keep the check logic in a maintainable state. Here enter the Angular router route guard which I also described few weeks ago in my post on how we could create and manage routes with the Angular router. Today I will show how we can use both together to solve the issue of ensuring data is loaded before displaying a route.
Let start back from the previous sample we built in the previous ngrx store post. You can browse the project before the changes made to demonstrate this blog post here.
The sample was having a selection of users which when choosen would load data from a service.
The goal of the post was to demonstrate how ngrx-store works. Therefore in select-user.ts
, the list of user was hardcoded in the template directly:
1
2
3
4
5
6
<select (change)="select($event.target.value)">
<option value=""> -- Select a user -- </option>
<option value="joe">Joe</option>
<option value="kim">Kim</option>
<option value="mike">Mike</option>
</select>
But this previous post left us with the following questions:
In order to answer those questions, we will first start by modify the sample to require a pre-loading of users before the app can be used.
We start by changing select-user.ts
.
1
2
3
4
5
6
7
8
9
10
11
12
13
@Component({
selector: 'app-select-user',
template: `
<select (change)="select($event.target.value)">
<option value=""> -- Select a user -- </option>
<option *ngFor="let user of users$ | async"></option>
</select>
`,
styles: []
})
export class SelectUserContainer implements OnInit {
users$: string[];
}
We then add an action and together with a list of user in the state saved by the reducer. https://github.com/Kimserey/ngrx-store-sample/commit/13d9eccdf8aef563f40840238c93a02ddb2b3d80
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
export const LOAD_ALL = '[User] Load All';
export const LOAD_ALL_SUCCESS = '[User] Load All Success';
export const LOAD_ALL_FAIL = '[User] Load All Fail';
export class LoadAllAction implements Action {
readonly type = LOAD_ALL;
constructor(public payload?: any) { }
}
export class LoadAllSuccessAction implements Action {
readonly type = LOAD_ALL_SUCCESS;
constructor(public payload: string[]) { }
}
export class LoadAllFailAction implements Action {
readonly type = LOAD_ALL_FAIL;
constructor(public payload?: any) { }
}
Then we continue by adding the effect to load a list of users. https://github.com/Kimserey/ngrx-store-sample/commit/13e1e208850916d989a7d6271152c4ac55505655
1
2
3
4
5
6
7
8
@Effect()
loadAll$: Observable<Action> = this.actions$
.ofType(user.LOAD_ALL)
.switchMap(() => {
return this.service.getAll()
.map(users => new user.LoadAllAction(users))
.catch(() => of(new user.LoadAllFailAction()));
});
And we finish with the reducer with the selector. https://github.com/Kimserey/ngrx-store-sample/commit/e55f1ca7f5e7e9854a6e45fda63b88e61f96576e
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
export interface State {
users: string[];
profile: Profile;
failure: boolean;
}
export const initialState: State = {
users: [],
profile: null,
failure: false
};
export function reducer(state = initialState, action: user.Actions) {
switch (action.type) {
case user.LOAD_ALL_SUCCESS: {
return Object.assign({}, state, {
users: action.payload
});
}
...
}
}
A guard is a service implementing CanActivate
. It will be registered on the route. CanActivate
expects the implementation of a single function canActivate
which returns a boolean or a promise of a boolean or an observable of boolean. In the case of observable on once the observable completes will the guard take the last item to decide whether the user can or not access the component.
In our case what we want is to:
new user.LoadAllAction()
This logic translate to the following guard:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Injectable()
export class UserLoadedGuard implements CanActivate {
constructor(private store: Store<fromRoot.State>) { }
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
// 1
const isLoaded$ = this.store.select(fromRoot.getUsers)
.map(users => users.length > 0);
// 2
isLoaded$
.take(1)
.filter(loaded => !loaded)
.map(() => new user.LoadAllAction())
.subscribe(this.store);
// 3
return isLoaded$
.take(1);
}
}
Then we can add this guard as a provider.
1
2
3
4
5
6
7
8
@NgModule({
...
providers: [
UserLoadedGuard,
...
]
})
export class AppModule { }
Now that we have the guard, we can use it in the route definition to protect the component.
1
2
3
4
5
6
7
8
9
10
11
export const routes: Routes = [
{
path: '',
canActivate: [UserLoadedGuard],
component: MainContainer,
children: [{
path: ':userId',
component: UserContainer
}]
}
];
Now everytime we navigate to the application, the guard will be excuted and the users will be loaded.
Why is it important?
Utilizing a guard has multiple advantages:
canActivate
is very simple and easily understood.Today we continued to look at how we could improve our implementation with ngrx-store. We saw how and where we could preload data by using Angular guard. The scenario exposed in this post is rather simple as the loading could have been placed in the container. But when multiple containers need to have the same data loaded, the guard is the way to go as it is a reusable way to ensure data is preladed. Hope you liked this post as much as I liked writing it. If you have any question, leave it here or hit me on Twitter @Kimserey_Lam. See you next time!