I'm facing the error expression has changed after it was checked
. I know, that this check runs only in dev mode (runs twice) and that I won't have the error message in prod. But I want to know why I get this.
I have a signal-based loader service like this:
import { Injectable, signal } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class LoaderService {
private _isLoading = signal<boolean>(false);
get isLoading(): boolean {
return this._isLoading();
}
show() {
this._isLoading.update(() => true);
}
hide() {
this._isLoading.update(() => false);
}
}
I have also this interceptor:
import {
HttpEvent,
HttpHandler,
HttpInterceptor,
HttpRequest,
HttpResponse,
} from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable, catchError, map, throwError } from 'rxjs';
import { USE_LOADING_SPINNER } from '../constants/app.constants';
import { LoaderService } from '../../shared/services/loader.service';
@Injectable()
export class LoadingInterceptor implements HttpInterceptor {
constructor(private loaderService: LoaderService) {}
intercept(
req: HttpRequest<any>,
next: HttpHandler
): Observable<HttpEvent<any>> {
if (req.context.get(USE_LOADING_SPINNER)) this.loaderService.show();
return next.handle(req).pipe(
map((event: HttpEvent<any>) => {
if (event instanceof HttpResponse) this.loaderService.hide();
return event;
}),
catchError((error) => {
this.loaderService.hide();
return throwError(() => error);
})
);
}
}
The USE_LOADING_SPINNER
is always set to true, which means I have to mark only the requests where I don't want to have any loading indicator.
Since the LoaderService
is signal-based, I register an effect
in the constructor of a component like this:
constructor() {
effect(() => {
this.loading = this.loaderService.isLoading;
});
}
Note the loading
variable isnt't a signal, it is a pure boolean variable, since the isLoading
method returns a boolean value, not a signal.
And in the template I have this in a grid component:
[loading]="loading"
This setup is working, on initial routing the loading indicator is showing.
Now I add a refresh method to the component like this:
refresh() {
//this.loaderService.show();
this.gridData$ = this.userService.getUsers();
//.pipe(tap(() => this.loaderService.hide()));
}
But if I click on refresh, I get the error in the topic title and the loading indicator isn't showing. The interceptor gets triggered, I checked this.
But if I uncomment the calls to the loaderService
it works as expected.
Why do I get this error?
I think the error expression has changed after it was checked
might be due to the update of loading
after the view has been initialized. Instead go for a signal based update, which might not trigger this error at all.
I think it is due to improper use of getter
and signal
, instead of executing the signal at the service level, return the actual signal.
When you want to do something with the previous state, then use update
, else set is a better option.
Your signal is private and you just want to return a readonly version of the signal, so we use asReadonly()
to remove the writable operations set
and update
when the component uses this signal.
import { Injectable, signal } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class LoaderService {
private _isLoading = signal<boolean>(false);
get isLoading(): boolean {
return this._isLoading.asReadonly(); // prevent modification of the signal
}
show() {
this._isLoading.set(true);
}
hide() {
this._isLoading.set(false);
}
}
Now, on the component level, instead of effect
which is overkill, just assign the service property to a component property.
// or
// loading = this.loaderService.isLoading;
constructor() {
this.loading = this.loaderService.isLoading;
}
At the template level, execute the signal.
[loading]="loading()"