Skip to content

Understanding switchMap and forkJoin operators in RxJS with TypeScript

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.

user-addresses

However, in a real-world scenario, the system can provide a RESTful service to get the list of the Users(ie. getUsers()).

users

Then, you'll need to perform another HTTP call to get the address for each of the previous users (i.e. getAddress(userId)).

address-by-user-id

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 applying getAddress() 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 of Observable<Address>.
  • users.forEach iterates over the users array to add every Observable<Address> to the previous addressArray$ 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[]>

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.