Reputation: 21
I am currently refactoring an Angular component that uses RxJS, aiming to implement signals. The component's requirements are:
Here's the current implementation of my component:
class BusinessDetailPage implements OnInit {
business: Business = null;
loading = true;
error = false;
trigger$ = new Subject<void>();
constructor(/** ... */) {}
ngOnInit() {
from(this.trigger$)
.pipe(
untilDestroyed(this),
tap(() => (this.loading = true)),
switchMap(() =>
this.businessService
.getBusiness(parseInt(this.route.snapshot.params.id))
.pipe(
first(),
catchError(() => {
this.error = true;
this.loading = false;
return of(null);
})
)
)
)
.subscribe((business) => {
this.business = business;
this.loading = false;
});
}
retry() {
this.trigger$.next();
}
}
Inspired by an example I found (signal-error-example), I refactored my component as follows:
export class BusinessDetailPage {
trigger$ = new Subject<void>();
business = toSignalWithError(this.fetchBusiness());
isLoading = computed(() => !this.business()?.value && !this.business()?.error);
constructor(/** ... */) {
this.trigger$.next();
}
refresh() {
this.trigger$.next();
}
private fetchBusiness() {
return from(this.trigger$).pipe(
untilDestroyed(this),
switchMap(() =>
this.businessService.getBusiness(
parseInt(this.route.snapshot.params.id)
)
)
);
}
}
This approach looks clean, but I'm unsure how to effectively display the loading indicator, particularly when an error or content is already present.
Alternatively, I considered a slightly more verbose approach:
export class BusinessDetailPage {
trigger$ = new Subject<void>();
business = toSignal(this.fetchBusiness());
error = signal(false);
isLoading = signal(false);
constructor(/** ... */) {
this.trigger$.next();
}
refresh() {
this.trigger$.next();
}
private fetchBusiness() {
return from(this.trigger$).pipe(
untilDestroyed(this),
tap(() => {
this.error.set(false);
this.isLoading.set(true);
}),
switchMap(() =>
this.businessService
.getBusiness(parseInt(this.route.snapshot.params.id))
.pipe(
catchError(() => {
this.error.set(true);
return of(null);
}),
finalize(() => this.isLoading.set(false))
)
)
);
}
}
This approach seems to cover all requirements but my goal was to derive (compute) the isLoading
from the signal, which is not the case anymore.
Upvotes: 2
Views: 503
Reputation: 804
I think the best scenario is to combine rxjs and signals for a sake of efficient reactivity.
See this stackblitz bro.
Some pieces of the code:
Service:
@Injectable({ providedIn: 'root' })
export class BusinessService {
businessData = signal<any | null>(null);
loading = signal(false);
error = signal(false);
router = inject(Router);
http = inject(HttpClient);
#loadingQueue = new Subject<number>();
loadingQueue = this.#loadingQueue.pipe(
switchMap((id) => {
this.error.set(false);
this.loading.set(true);
const simulateError = Math.random() < 0.5;
return this.http
.get(
`https://jsonplaceholder.typicode.com/todos${
simulateError ? 'x' : ''
}/${id}`
)
.pipe(
catchError(() => {
this.error.set(true);
return of(null);
}),
tap((response) => {
this.businessData.set(response);
}),
finalize(() => this.loading.set(false))
);
})
);
loadBusiness(id: number) {
this.loading.set(true);
this.#loadingQueue.next(id);
}
changeBusiness() {
this.router.navigate(['business', Math.floor(Math.random() * 100)]);
}
}
Component:
@Component({
selector: 'app-business',
standalone: true,
templateUrl: './business.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [NgIf, JsonPipe],
})
export class BusinessComponent {
id = input.required<number>();
businessService = inject(BusinessService);
constructor() {
this.businessService.loadingQueue.pipe(takeUntilDestroyed()).subscribe();
effect(() => {
const id = this.id();
untracked(() => this.businessService.loadBusiness(id));
});
}
}
HTML:
<h3>The business {{ id() }}</h3>
<h5>
<button
[disabled]="businessService.loading()"
(click)="businessService.changeBusiness()"
>
Change business
</button>
</h5>
<p *ngIf="businessService.error()" style="color: red; font-weight: bolder;">
Error occured!
</p>
<pre *ngIf="businessService.businessData() as data">{{ data | json }}</pre>
Bootrapping the App:
@Component({
selector: 'app-root',
standalone: true,
template: `
<h1>Hello from {{ name }}!</h1>
<router-outlet></router-outlet>
`,
imports: [RouterOutlet],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class App {
name = 'Angular';
}
bootstrapApplication(App, {
providers: [
provideRouter(
[
{
path: '',
pathMatch: 'full',
redirectTo: 'business/1',
},
{
path: 'business/:id',
loadComponent: () =>
import('./business.component').then((c) => c.BusinessComponent),
},
],
withComponentInputBinding()
),
provideHttpClient(),
],
});
Upvotes: 2