Apply an API Request to Each Entity in a List with Reactive Loading Status Tracking
This article shows how to apply an API request to each entity of a list, display its loading status, and execute requests in parallel.
You'll find an example you can reuse in your applications.
Here is an example:
To make this easily, I will introduce you to the groupBy
operator combined with another custom operator I often use, called statedStream
.
Create the "statedStream" Operator to Track API Request Loading Status
The statedStream
operator helps track the loading status of an asynchronous request. I usually use it during API calls.
Its behavior is similar to Angular's httpResource
, but we stay within the world of observables.
updateItem$
.pipe(
switchMap((updateItem) => // everytime, updateItem$ emits a new value, it cancels the existing API call and creates a new API call
statedStream(updateApiCall$(updateItem), updateItem)
)
)
.subscribe((data) => console.log('data', data));
Instead of waiting to receive a single value once the API call is complete, statedStream
emits an initial value indicating that the request is loading (isLoading: true
).
Here is part of the statedStream
code:
export function statedStream<T>(
toCall: Observable<T>,
initialValue: T
): Observable<SatedStreamResult<T>> {
return toCall.pipe(
map(
(result) =>
({
isLoading: false,
isLoaded: true,
hasError: false,
error: undefined,
result,
} satisfies SatedStreamResult<T>)
),
startWith({
isLoading: true,
isLoaded: false,
hasError: false,
error: undefined,
result: initialValue,
}),
catchError((error) =>
of({
isLoading: false,
isLoaded: false,
hasError: true,
error,
result: initialValue,
})
)
);
}
Note: If you're not familiar with observable streams, remember that when an API call returns an error, your stream stops listening for further emissions (here, updateItem$
).
Thanks to statedStream
, errors are caught and handled within the result, allowing the stream to continue emitting new requests.
Here is a Stackblitz link to see this function in detail.
Unlock the Titanic Potential of the groupBy
Operator
Have you ever used the groupBy
operator from RxJs? Personally, when I first read the documentation, I didn't get it. The tenth time... still no. But thanks to this example, I finally understood!
You can also check the documentation and an example on Stackblitz.
Basically, we reuse the statedStream
example and add it into the groupBy
stream:
updateItem$
.pipe(
groupBy((updateItem) => updateItem.id), // create a group for each unique id
mergeMap((group$) => {
console.log('group$', group$.key);
return group$.pipe(
switchMap((updateItem) =>
statedStream(updateApiCall$(updateItem), updateItem)
)
);
})
)
.subscribe((data) => console.log('Received:', data));
Then, we emit some updates and you'll see how it works:
console.log("emit updateItem first time", 'id: 4')
updateItem$.next({
id: '4',
name: 'Romain Geffrault 4',
});
console.log("emit updateItem first time", 'id: 5')
updateItem$.next({
id: '5',
name: 'Romain Geffrault 5',
});
setTimeout(() => {
console.log("emit updateItem second time", 'id: 4')
updateItem$.next({
id: '4',
name: 'Romain Geffrault 4, updated twice',
});
}, 5000)
Result:
Thanks to groupBy
, it's simple to launch multiple API requests in parallel.
Here’s the link to see it in action.
In this example, I grouped by ID, which is a basic case, but you can push the concept further.
Display a List of Entities with Reactive Loading Status in Angular
Let’s be pragmatic: here's an implementation close to a real-world case that you can reuse easily.
Unfortunately, the Stackblitz link doesn't work, but here’s the GitHub repo you can clone to try it out.
I used NodeJs v20.
npm i
ng serve
The pages you should check are:
- src\app\features\data-list\data-list.component.ts
- src\app\features\data-list\data-list.component.html
I added many comments to explain how some RxJs functions work if you're not used to them.
I used a declarative/reactive approach here, which is my preferred way due to its many advantages.
You’ll notice I managed cases where API calls finish before unsubscribing the streams (thus preventing memory leaks and weird behaviors).
I love this example, but it can still be improved.
For instance, I had to repeat the data types multiple times for TypeScript.
Even though it's not that hard to add, I didn't handle keeping the existing list displayed during navigation, nor adding selectors easily...
Another point: despite everything, all these code snippets take up space and hurt the overall component visibility.
A solution could be to split the different cases inside the scan
into separate functions, similar to reducers. But it might be a bit tricky to properly infer types without help.
We could also imagine applying a bulk action to several items at once (bulkEdit, etc.).
I've taken all these points into account and am currently creating a small experimental tool that will allow me to implement these mechanisms declaratively.
It’s somewhat similar to a server-state management tool, like TanStackQuery. It still needs some thinking, but I can’t wait to show you the result.
If you have any questions or would like to discuss it, feel free to comment — I’ll be happy to answer as best as I can.
P.S.: I didn’t use signals because:
- I want to apply this pattern in apps that aren't yet using the latest Angular versions.
- You can easily convert to a signal if needed.
- More importantly, the updates/deletes are triggered by events, not states — which observables handle perfectly but signals don't.