One of Angular's pillars is RxJS. So, we need to change the way we think about services, methods, variables.
Let's talk about your service
AuthService makes a call to your backend and figure out if the user is authenticated. AuthGuard uses this service to do something. As you can see from syntax of canActivate, Angular expects you to return one of the following types
Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree
What AuthGuard returns now is boolean | UrlTree. You can also return Observable<boolean | UrlTree>. This should tell us something. AuthService has an internal Observable however, it subscribes to it and does not return it. So let's do some refactor
export class AuthService {
private apiUrl = environment.api_uri;
constructor(private http: HttpClient) { }
public authorize(): Observable<boolean> {
return this.http.get(this.apiUrl+'/auth')
.pipe(
map(data => {
// some logic here
let authorized = ....
return authorized;
}),
catchError(error => {
/** do some error handling
* I assume you wouldn't activate routing in case of an error
* Also, you need to wrap your return value in an `Observable`
* of is the simplest way to do that
*/
return of(false);
}));
}
}
Now that AuthService returns an Observable we can use some operators on top of that and return it from AuthGuard.
export class AuthGuard implements CanActivate {
constructor(private auth: AuthService, private router: Router){}
canActivate(
next: ActivatedRouteSnapshot,
state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
return this.auth.authorize().pipe(
map(isAuthorized => isAuthorized || this.router.parseUrl("/403"))
);
}
}
What I just suggested made your service stateless which means it does not hold any data. It just does whatever it does again every time someone calls one of its methods.
Sometimes, our services needs to hold some data and new comers should get this old retrieved data. In this case, we can use Subject, BehaviorSubject or shareReplay operator (which is my favorite).
Let's talk about Subject.
For more info, you can read it here
What is a Subject? An RxJS Subject is a special type of Observable that allows values to be multicasted to many Observers. While plain Observables are unicast (each subscribed Observer owns an independent execution of the Observable), Subjects are multicast.
So let's create a Subject (from the docs)
const subject = new Subject<number>();
const subscriber1 = subject.subscribe(val => console.log(val))
const subscriber2 = subject.subscribe(val => console.log(val))
// this will trigger callback functions of the subscribes above
subject.next(5);
However, late comers won't get the value 5 (last emitted value)
following console.log statement won't execute
const subscriber3 = subject.subscribe(val => console.log(val))
If you care about new comers to be able to retrieve what was emitted before, you can simply use BehaviorSubject
For more info, you can read it here
const subject = new BehaviorSubject(123);
// two new subscribers will get initial value => output: 123, 123
subject.subscribe(console.log);
subject.subscribe(console.log);
// two subscribers will get new value => output: 456, 456
subject.next(456);
// new subscriber will get latest value (456) => output: 456
subject.subscribe(console.log);
// all three subscribers will get new value => output: 789, 789, 789
subject.next(789);
This is pretty useful if you should be able to update the "source" later.
However, in some cases, different components need the same data and it never changes and you only want to make the http call once. Like the user begin authenticated (in your case).
Using BehaviorSubject will require some initial value and your subscribers will get this value as well. You may not want that. Instead, let's use shareReplay
Let's refactor AuthService a bit.
I'll create a class member called isAuthorized (an Observable)
I'll use the same logic from above and since it is an Observable, it will not make the call until someone subscribes to it.
And finally, I'll add a shareReplay operator to make sure that this request will made only once.
export class AuthService {
private apiUrl = environment.api_uri;
private isAuthorized = this.http.get(this.apiUrl+'/auth')
.pipe(
map(data => {
// some logic here
let authorized = ....
return authorized;
}),
catchError(error => {
return of(false);
}),
shareReplay()
);
constructor(private http: HttpClient) { }
}
Now you can use it as follows and no matter how many times you use it, the underlying http call will be executed only once.
authService.isAuthorized.subscribe(_ => .....)