id | title |
---|---|
part-4 |
Part 4. How RxJS is used by Angular |
Contributors:
- Andrei Gatej
Writer: Andrei Gatej
In this chapter, we're going to expose which parts of Angular are powered by RxJS, along with some practical examples.
If you'd like to follow along, you could open this StackBlitz demo.
Making requests over the network definitely complies with a well known Observable definition: data which comes over time. With this in mind, an HTTP request can be seen as an Observable that will emit some data at some point in the future. Let's see how it would look like:
users$: Observable<any[]> = this.http.get<any[]>(this.url);
constructor (private http: HttpClient) { }
The HttpClient.get(url) method will perform a GET request to the specified url. This method(and the similar ones, e.g post, put etc...) will return an Observable which will emit once the response is ready and then it will emit a complete notification. This implies that there is no need to explicitly unsubscribe from an observable returned from HttpClient.
In order to get a better understanding, here's how you could loosely implement something similar to what the HttpClient does:
new Observable((subscriber) => {
// Make the request here, e.g using `fetch` or `XMLHttpRequest`
// Then, after the response(`resp`) is ready
subscriber.next(resp);
subscriber.complete();
});
Another great feature the HttpClientModule provides is the ability to intercept and alter the requests and their responses. This can be achieved with Interceptors.
For example, this is how an interceptor can be provided:
{ provide: HTTP_INTERCEPTORS, useClass: TokenInterceptor, multi: true }
And this is how it might be implemented:
@Injectable()
export class TokenInterceptor implements HttpInterceptor {
intercept(
req: HttpRequest<any>,
next: HttpHandler
): Observable<HttpEvent<any>> {
// Altering the incoming request, e.g: adding necessary headers
const newReq = req.clone({
setHeaders: { Authorization: "<schema-type> <credentials>" },
});
return next.handle(newReq).pipe(
// Altering the response
// Only interested in `Response` events. Others could be: `Sent`, `UploadProgress` etc...
filter((e) => e.type === HttpEventType.Response)
// Can also retry requests, maybe the authorization token expired, so we can use the refresh token to get a new one
// catchError(err => handleExpired()),
);
}
}
As you can see, interceptors come up with a lot of possibilities. This is possible because RxJS' Observables can be composed.
For example, you might have something like this:
const be$ = new Observable((subscriber) => {
setTimeout(() => {
// After the request is ready
subscriber.next({ data: {}, err: null });
subscriber.complete();
}, 3000);
});
// Applying an interceptor
const intercepted$ = be$.pipe(filter(/* ... */), catchError(/* ... */));
// Applying another interceptor
const interceptedTwice$ = intercepted$.pipe(/* ... */);
If you'd like to follow along, you could open this StackBlitz demo.
Sometimes, when working with Angular Forms, you might need to perform certain actions when the value or the status of a form control changes. You can be notified about these events with the help of valueChanges and statusChanges.
Let's see how they can be used:
this.valueChangesSub = this.myCtrl.valueChanges.subscribe((v) => {
console.log("value changed", v);
});
this.statusChangesSub = this.myCtrl.statusChanges.subscribe((v) => {
console.log("status changed", v);
});
When typing into the input, both the registered callbacks will be called. valueChanges emits when the control's value has changed and statusChanges when the control's status changed(e.g from INVALID to VALID).
As with every observable, you can apply operators to it. For instance, you might want to be notified only when the current status is different than the previous one:
this.statusChangesSub = this.myCtrl.statusChanges
.pipe(distinctUntilChanged())
.subscribe((v) => {
console.log("status changed", v);
});
One thing that you should be mindful of is that you don't have to unsubscribe from these observables when the component is destroyed because the data producer(e.g valueChanges) belongs to the component in question, thus everything becomes eligible for garbage collection.
If you'd like to follow along, you could open this StackBlitz demo.
You might be familiar with ViewChildren and ContentChildren decorators. They can be used to query elements from a component's view and from a component's projected content, respectively. Both return a QueryList type.
One thing that might come handy in certain situations is to be notified about changes that occur in the list obtained, changes such as addition or removal. This is possible as the QueryList structure exposes a changes property which emits whenever the actions delineated above take place.
Here is an example of ViewChildren:
<ul>
<li *ngFor="let _ of [].constructor(total); index as idx" #item>
{{ idx + 1 }}
</li>
</ul>
<hr />
<br />
<button (click)="total = total - 1 < 0 ? 0 : total - 1">Remove Item</button>
<button (click)="total = total + 1" style="margin-left: 3rem;">Add Item</button>
And the corresponding TS file:
@Component({ ... })
export class AppComponent {
total = 10;
@ViewChildren('item') private items: QueryList<HTMLUListElement>;
ngAfterViewInit () {
this.items.changes.subscribe(changes => {
console.log('changes occurred', changes);
});
}
}
One thing that is worth mentioning is that you don't have to unsubscribe when the component is destroyed as this is handled internally by QueryList.
If you'd like to follow along, you could open this StackBlitz demo.
Another significant part where Angular heavily uses RxJS is routing.
For instance, when you want to manage the access to certain routes, you can leverage guards.
Each guard can return, among others, Observables, allowing you to perform complex logic when deciding whether the route should be accessed or not.
Let's see an example of CanActivate:
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
// The logic might involve Observables
return timer(1000).pipe(
map(() => true),
);
}
Note: the same concept can be applied to other guards.
Observables can also be used when implementing route resolvers:
export class HelloResolver implements Resolve<any> {
constructor(private http: HttpClient) {}
resolve(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<any> | Promise<any> | any {
return this.http
.get<any>("https://jsonplaceholder.typicode.com/posts")
.pipe(map((arr) => arr.slice(0, 10)));
}
}
The result of the resolver can then be accessed in the component route's component through the ActivatedRoute.data property.