I want to show a loader when async loading an Angular route config, however sometimes these can load very fast and the loader just flashes on screen for a split second.
I want to instead only show the loader if it has not finished loading after a period of time. My first thought was to use delay(250)
. This works when navigating to that page with the needed config, but if I refresh that page it seems to always flash the loader on screen for a brief moment
Here’s what I am doing right now
private isLoadingRouteConfig = false;
readonly isLoadingRouteConfig$ = this.router.events.pipe(
delay(250), //if the config loads very quickly, don't even show this
map((ev) => {
if (ev instanceof RouteConfigLoadStart) {
this.isLoadingRouteConfig = true;
} else if (ev instanceof RouteConfigLoadEnd) {
this.isLoadingRouteConfig = false;
}
return this.isLoadingRouteConfig;
}),
);
@if (isLoadingRouteConfig$ | async) {
<app-loader />
}
How can I make this work as I would expect it to? I want to show a loader after 250ms, but skip this if it has completed loading already.
I believe flashing is occurring because it’s delay before emitting true and false, so even if the events occur instantaneously after each other, you’ll still have on 250ms for the values to changes.
The observable stream below will fix this situation by do this following:
- Subscribe to router events.
- Filters out all events but RouteConfigLoadStart and RouteConfigLoadEnd.
- Uses switchMap to listen to an inner observable. This operator is used because of its cancelling effect.
- If the event is a start event, then a timer begins. When it completes it will emit true. If another event happens before 250ms then it will not emit.
- If the event is an end event, then false is immediately emitted.
- Immediately emits false even if no qualifying router events have occurred.
readonly isLoadingRouteConfig$ = this.router.events.pipe(
filter((ev) => (ev instanceof RouteConfigLoadStart) || (ev instanceof RouteConfigLoadEnd)),
switchMap((ev) =>
(ev instanceof RouteConfigLoadStart)
? timer(250).pipe(map(() => true)
: of(false)
),
startWith(false)
);
There might be other ways to handle this, with delayWhen perhaps. Also I took the setting of isLoadingRouteConfig out. Feel free to add it to the end of the stream by using tap(x => this.isLoadingRouteConfig = true)
, but I’d encourage you to try to avoid using it. If you can’t using a flattening operator then try consider trying toSignal.
1
Maybe something like this will work for you:
const isLoadingRouteConfig$ = this.router.events.pipe(
filter(ev => ev instanceof RouteConfigLoadStart || ev instanceof RouteConfigLoadEnd),
switchMap(ev => ev instanceof RouteConfigLoadStart
? timer(250).pipe(map(() => true), startWith(false))
: of(false)
),
);
The idea is that when you receive an even you caret about, you use switchMap
to “switch” to a new observable source.
On RouteConfigLoadStart
you emit an observable that immediately emits false
, then after 250ms, emits true
.
On RouteCofigLoadEnd
you emit an observable that simply emits false
immediately.
If the RouteConfigLoadEnd
is received before 250ms, the true
value will never be emitted, because the source observable was already switched.
Your delay operator will also delay the RouteConfigLoadEnd event, which I assume you do not want. Try using timer instead of delay, and switchMap to cancel the timer if RouteConfigLoadEnd is received:
readonly isLoadingRouteConfig$ = this.router.events.pipe(
filter(ev => ev instanceof RouteConfigLoadStart || ev instanceof RouteConfigLoadEnd),
switchMap(ev => (ev instanceof RouteConfigLoadStart ? timer(250).pipe(map(() => true)) : of(false))));
Jon Arne Grødal is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.