RxJS Mastery – #30 expand

RxJS expand lesson title

The RxJS expand operator works recursively. As a transformation operator it offers a mapping function or projection. This mapping function is not only applied on the source Observable, but also again on the output. This is how the recursion is caused.

Recursively project with RxJS expand

In programming we know that a stop condition is necessary as soon as recursion comes into play. And it’s no different for expand. Let’s have a look at a basic example that output the powers of 2 but only until 1024:

const result$ = of(2).pipe(
    expand(x => of(x * 2)),
    take(10),
);

result$.subscribe(console.log);
// 2
// 4
// 8
// ...
// 1024

In above example the stop is caused by take(10). After 10 values we complete and therefore also complete the expand operation.

Stop RxJS expand by emitting EMPTY

Because expand always works on the inner Observable when re-applying the mapping we also have another way to stop the operation. If expand is receiving an EMPTY Observable it also stops because empty cannot be projected into anything:

const result$ = of(2).pipe(
    expand(x => x === 1024 ? EMPTY : of(x * 2)),
);

result$.subscribe(console.log);
// 2
// 4
// 8
// ...
// 1024

As soon as the value supplied to the project function of expand is equal to 1024 the EMPTY Observable is emitted. EMPTY just emits complete and therefore expand stops.

Expand to go from page to page

Let’s imagine there are API endpoints delivering page content in the form of:

interface Page {
    content: string;
    next: number;
}

where next points to the next page. In this case we can use expand to go from page to page. This can happen until no next field is found anymore or until a certain limit:

const paginatedHttpService = {
    getPage: (page: number) => of({content: `Lorem ipsum on page ${page}`, next: page + 1})
};

paginatedHttpService.getPage(1).pipe(
    expand(response => response.next ? paginatedHttpService.getPage(response.next) : EMPTY),
    take(3),
).subscribe({
    next: (v) => console.log(`${v.content}`),
    complete: () => console.log('all pages or max 3 pages returned'),
});

We get the page content of the first three pages like that:

Lorem ipsum on page 1
Lorem ipsum on page 2
Lorem ipsum on page 3
all pages or max 3 pages returned

So, expand can help when http requests need to be executed recursively and especially when they need to take into account the previous request’s response.

Exercise for the expand operator 💪

Use expand to implement a retry mechanism for http calls based on the following service:

const httpService= {
    get: (url: string, attempt: number) => throwError('This is an error!').pipe(
        catchError(_ => of({error: 'Error', attempt: attempt}))
    )
};

As always the code examples and exercise solution can be found on GitHub.