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.
Top comments (6)
OK. Thanks for your help
My take away is that
groupBy
groups objects together, using a specific key, into an array of objects.I guess if I keep my mental model simple then, I might be able to remember it.
As far as
higher order operators
go, I found this analogy:Analogy
switchMap: Like a chef who starts preparing one order but stops and throws it away to start preparing a new, incoming order immediately.
exhaustMap: Like a barista who ignores a new customer order if they are already making a coffee for another customer
This is a great article, but why can’t you just use the
mergeMap
higher order operator, rather than thegroupBy
strategy. ThemergeMap
operator allows for parallel inner observable processing?I get that the
groupBy
operator adds similar objects to an array, based on a chosen key, which, in this case, isupdateItem.id
. But what is the purpose of this, when using an infinite scroller?Nice question! You can, but using mergeMap is very limited — what if you want to apply a concatMap, switchMap, or exhaustMap strategy per group?
For example:
– You enable updating users’ names concurrently.
– If a name of the same user is updated multiple times in a short period, you want to cancel the previous calls and keep only the latest one effective.
Using mergeMap doesn’t allow this behavior because all requests will run concurrently, which may lead to race conditions issue.
Using groupBy + switchMap is perfect for this case.
Hope this helps! :D
So, you are saying you could apply
switchMap
to one group, amergeMap
to another and anexhaustMap
to another?So I guess you reference each group by its array index?
Like
Yes, it’s possible, even if it’s not very common.
Maybe my previous example wasn’t explicit enough, so I tried to create another one to help you understand the difference between mergeMap and groupBy.
mergeMap vs groupBy and switchMap
In this demo, if you click multiple times on the “Update name using mergeMap” button, all the async calls will be taken into account.
But if you click multiple times on the “Update name using groupBy + switchMap” button, only the latest async call will be taken into account per user.
Let me know what you think about this demo!
Cheers. I really appreciate this. I can see that
groupBy
looks really powerful but I can’t quite grasp it yet! I will take a look at your new example and see if it helps.