RxJS is a library with more than 22 Million downloads per week. It's widely used in the JavaScript world, and supports TypeScript as well.
I recently saw the following question in a developer forum:
Is there a way to handle a list of Observables and get the final result of them once they're completed?
Right after reading that question, I started to think in a reactive approach in time to come up with a "pattern" that can be used in various scenarios.
The Problem
Let's suppose you're building an application that needs to handle a series of asynchronous calls to display the users, and their respective addresses.
However, in a real-world scenario, the system can provide a RESTful service to get the list of the Users(ie. getUsers()
).
Then, you'll need to perform another HTTP call to get the address for each of the previous users (i.e. getAddress(userId)
).
Let's solve this problem in a Reactive Approach using RxJS operators and TypeScript.
Define the Data Model
Let's rely on TypeScript interfaces and powerful static typing to have the model ready.
// user.ts
export interface User {
id: number;
name: string;
address?: Address;
}
The User interface defines the address
attribute as optional and that means it can be undefined
if the user doesn't "contain" a valid address value yet.
// address.ts
export interface Address {
country: string;
state: string;
city: string;
street: string;
zipCode: number;
}
The Address interface displays a set of attributes that represents a complete address.
The Data Example
To make testing easier, let's define a dataset for the entities we defined already in the model.
// index.t
import { Address, User } from "./address";
import { User } from "./user";
const users: User[] = [
{
id: 0,
name: "Donald Mayfield"
},
{
id: 1,
name: "Jill J. Fritz"
},
{
id: 2,
name: "Terry Buttram"
}
];
const address: Address[] = [
{
street: "2180 BELLFLOWER",
country: "USA",
state: "AL",
city: "Madison",
zipCode: 35064
},
{
street: "845 ODOM ROAD, SUITE 200",
country: "USA",
state: "CA",
city: "Los Angeles",
zipCode: 90720
},
{
street: "9025 QUEENS BLVD",
country: "USA",
state: "NY",
city: "Queens",
zipCode: 11355
}
];
Looks like we'll need a function to get an Address given a User id.
// index.ts
const getAddress = (userId: number) => of(address[userId]);
The previous function getAddress
will return an Address as an Observable: Observable<Address>
. This is possible by using the of
operator from RxJS, which is a "Creation Operator" that converts the arguments(an Address
object) to an observable sequence.
Processing the Data as Observables
Since we have the data model defined, it is time to create some variables that allow us to "contain" the set of users, and addresses, as Observables:
// index.ts
let users$: Observable<User[]>;
let address$: Observable<Address[]>;
Next, let's assign the appropriate value to the users$
variable:
users$ = of(users);
Again, the previous line is using the of
operator to create an observable from the users
array.
In the real world, the "users" data will come from a RESTful endpoint, a JSON file, or any other function that can perform an asynchronous call. You may expect an Observable as a result of that function or you can create it using an RxJS Operator.
Using switchMap and forkJoin Operators
Now it's time to process the set of Users and perform the getAddress(id)
call for every User:
address$ = users$.pipe(
switchMap((users: User[]) => {
// Iterate the 'users' array and perform a call to 'getAddress'
users.forEach(user => {
const address = getAddress(user.id);
// ...
});
})
);
What is happening here?
- users$.pipe(). This function call provides a readable way to use RxJS operators together(
switchMap
in this example). - switchMap() operator comes first and allows to process the data(
User[]
) which comes from the observable(Observable<User[]>
). Also, it allows returning an Observable as a result of applyinggetAddress()
call for every user.
However, we're not doing anything useful yet from getAddress()
results. Let's create an array to "store" them, and return a new Observable:
address$ = users$.pipe(
switchMap((users: User[]) => {
// Create and initialize the array
const addressArray$: Observable<Address>[] = [];
// Iterate over 'users' array
users.forEach(user => {
const address$: Observable<Address> = getAddress(user.id);
addressArray$.push(address$);
});
// [Observable<Address>, Observable<Address>, ....., Observable<Address>]
return forkJoin(addressArray$);
})
);
The following operations are performed inside switchMap
call:
- An empty array is created in order to "contain" the results of every
getAddress
call. This array will expect to contain a set ofObservable<Address>
. - users.forEach iterates over the
users
array to add everyObservable<Address>
to the previousaddressArray$
variable. - forkJoin(addressArray$). We have an array of Observables at this point and the
forkJoin
operator will do the following:- Accept the array of Observables as an input(
Observable<Address>[]
) - It will wait for every Observable(from the array) to complete and then combine the last values they emitted.
- Finally, it will return an array of
Address[]
as a new Observable:Observable<Address[]>
- Accept the array of Observables as an input(
As a final step, you'll need to subscribe as follows.
// Subscription
address$.subscribe((address: Address[]) => {
console.log({ address }); // Prints the array of addresses: Address[]
});
The result would be the following JSON object:
{
"address": [
{
"street": "2180 BELLFLOWER",
"country": "USA",
"state": "AL",
"city": "Madison",
"zipCode": 35064
},
{
"street": "845 ODOM ROAD, SUITE 200",
"country": "USA",
"state": "CA",
"city": "Los Angeles",
"zipCode": 90720
},
{
"street": "9025 QUEENS BLVD",
"country": "USA",
"state": "NY",
"city": "Queens",
"zipCode": 11355
}
]
}
Demo Project
Find the complete project running in StackBlitz. Don't forget to open the browser's console to see the results.