I'm starting with reactive programming and I have the following application:
The three dotted items is a <ul>
tag. Each time a li
item is clicked, the app navigates to a route using the routerLink appending a query string (/bands, /bands?active=false and /bands?active=true).
The part of the web page starting at Bands (belong to the component called band-list) is the content the router-outlet displays according to the route. The grid below is loaded according to the query param 'active'. Then, each time a li
item is clicked, the grid reloads.
Also, the Refresh data button reloads the grid.
I'm trying to implement, using reactive programming, the following logic:
At initialization, the
band-list
component should load the grid according to theactive
query parameter in the route.Each time the user clicks a
li
, the grid should reload. If previous to this click on ali
the grid is still loading (making an http request), the http request in progress should be cancelled (this looks to me like aswitchMap
operator).If the user clicks the
refresh data button
and the grid has finished loading its data, the grid should load. However, if the grid is still loading its data (in the middle of an http request), the click should do nothing, i.e., the in-process loading data operation should finished, and the click in the button is practically ignored (this looks like anexhaustMap
to me :S)Also, each time an http operation is performed, a message telling "Loading..." should show up while the http operation has no finished, and the data in the grid should be emptied. When the http request finishes, the "Loading..." message should be removed and the data should show up in the grid.
I implemented the code in the band-list
component like this next code block, but I'm not sure about the use of the defer
operator for my solution. I used the defer
operator since I need to maintain the state if a current request is in process with the isLoading
variable inside the defer operator, and also updating this variable as a side-effect action in the tap
operator, and using this variable as a side-effect in the filter
operator.
My question is: is this a nice way to do it according to reactive programming? What could I do better?
You can check this solution in this stackblitz' url
@Component({
selector: 'app-band-list',
templateUrl: 'band-list.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class BandListComponent {
readonly initialState: BandState = { data: [], isLoading: false };
#bandDataService = inject(BandDataService);
refreshDataClickSubject = new Subject<void>();
#activedRoute = inject(ActivatedRoute);
bandState$;
constructor() {
this.bandState$ = this.getBands$();
const destroyRef = inject(DestroyRef);
destroyRef.onDestroy(() => {
this.refreshDataClickSubject.complete();
this.refreshDataClickSubject.unsubscribe();
});
}
getBands$(): Observable<BandState> {
const queryParams$ = this.#activedRoute.queryParams;
const refreshDataClick$ = this.refreshDataClickSubject.asObservable();
const isActiveFromQueryParams$ = queryParams$.pipe(
map<any, boolean | undefined>(p => p["active"])
);
const final$ = defer(() => {
let isLoading = false;
const getBandsFn$ = (isActive: boolean | undefined) => concat(
of([]).pipe(
tap(_ => isLoading = true), // <== here updating the outer state.
map(data => ({data, isLoading: true}))
),
this.#bandDataService.getBands$(isActive).pipe(
map(data => ({data, isLoading: false}))
)
).pipe(
finalize(() => isLoading = false) // <== again updating the outer state.
);
const obs$ = merge(
queryParams$.pipe(map(_ => RequestOrigin.QueryParams)),
refreshDataClick$.pipe(map(_ => RequestOrigin.RefreshButton))
).pipe(
filter(requestOrigin => requestOrigin === RequestOrigin.QueryParams ||
(requestOrigin === RequestOrigin.RefreshButton && !isLoading)),
withLatestFrom(isActiveFromQueryParams$),
map(([_, isActive]) => isActive),
switchMap(isActive => getBandsFn$(isActive)),
startWith(this.initialState)
);
return obs$;
});
return final$;
}
}
enum RequestOrigin {
QueryParams, RefreshButton
}
interface BandState {
data: Band[],
isLoading: boolean
}
<h2>Bands</h2>
<div class="row">
<div class="col-sm-12">
<div class="form-group">
<button class="btn btn-primary" type="button" (click)="refreshDataClickSubject.next()">
Refresh data
</button>
</div>
@if(bandState$ | async; as bandState) {
@if (bandState.isLoading) {
Loading...
} @else {
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Formation year</th>
<th>Is active</th>
</tr>
</thead>
<tbody>
@for(band of bandState.data; track $index) {
<tr>
<td>{{ band.name }}</td>
<td>{{ band.formationYear }}</td>
<td>{{ band.isActive }}</td>
</tr>
} @empty {
<h3>Empty!</h3>
}
</tbody>
</table>
}
} @else {
BandState$ not initialized
}
</div>
</div>
bands$
observable missing in band-data.service, which your are trying to access in line 74 of band-list.component. Same withisActiveChanged
. Could you please update your code.