Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Save more on your purchases! discount-offer-chevron-icon
Savings automatically calculated. No voucher code required.
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Newsletter Hub
Free Learning
Arrow right icon
timer SALE ENDS IN
0 Days
:
00 Hours
:
00 Minutes
:
00 Seconds
RxJS Cookbook for Reactive Programming
RxJS Cookbook for Reactive Programming

RxJS Cookbook for Reactive Programming: Discover 40+ real-world solutions for building async, event-driven web apps

Arrow left icon
Profile Icon Nikola Mitrović
Arrow right icon
$26.99
eBook Mar 2025 310 pages 1st Edition
eBook
$26.99
Paperback
$33.99
Subscription
Free Trial
Renews at €18.99p/m
Arrow left icon
Profile Icon Nikola Mitrović
Arrow right icon
$26.99
eBook Mar 2025 310 pages 1st Edition
eBook
$26.99
Paperback
$33.99
Subscription
Free Trial
Renews at €18.99p/m
eBook
$26.99
Paperback
$33.99
Subscription
Free Trial
Renews at €18.99p/m

What do you get with eBook?

Product feature icon Instant access to your Digital eBook purchase
Product feature icon Download this book in EPUB and PDF formats
Product feature icon Access this title in our online reader with advanced features
Product feature icon DRM FREE - Read whenever, wherever and however you want
OR
Modal Close icon
Payment Processing...
tick Completed

Billing Address

Table of content icon View table of contents Preview book icon Preview Book

RxJS Cookbook for Reactive Programming

Building User Interfaces with RxJS

One of the areas where RxJS excels is handling user interactions and orchestrating events in the browser. In this chapter, we’re going to explore how to build awesome and interactive UI components that handle any interaction or side effect seamlessly.

In this chapter, we’ll cover the following recipes:

  • Unlocking a phone with precision using RxJS-powered swipe gestures
  • Learning indications with the progress bar
  • Streaming image loading seamlessly with Progressive Image
  • Optimizing loading tab content
  • Reacting to drag-and-drop events
  • Crafting your perfect audio player using flexible RxJS controls
  • Streamlining real-time updates with RxJS-powered notifications
  • Fetching data with the Infinite Scroll Timeline component

Technical requirements

To complete this chapter, you’ll need the following:

  • Angular v19+
  • Angular Material
  • RxJS v7
  • Node.js v22+
  • npm v11+ or pnpm v10+

The code for the recipes in this chapter can be found in this book’s GitHub repository: https://github.com/PacktPublishing/RxJS-Cookbook-for-Reactive-Programming/tree/main/Chapter02.

Unlocking a phone with precision using RxJS-powered swipe gestures

How cool would it be to have a phone unlock pattern component? In this recipe, we’re going to build a component like that so that we can seamlessly react to every user touch swipe, orchestrate all user events, and unlock the phone once a correct combination of numbers is entered.

How to do it…

To create a phone unlock component, we’ll create UI controls representing number pads and identify key events to react to user actions. Once the user lifts their finger off the screen, we’ll compare the result with the correct pattern to unlock our phone.

Step 1 – Creating number pads

Our swipe-unlock.component.html file must contain the following markup for the swipe area and all phone buttons:

<div #swipeArea class="swipe-area">
    <button #one class="number">1</button>
    <button #two class="number">2</button>
    <button #three class="number">3</button>
    <button #four class="number">4</button>
    <button #five class="number">5</button>
    <button #six class="number">6</button>
    <button #seven class="number">7</button>
    <button #eight class="number">8</button>
    <button #nine class="number">9</button>
    <button #zero class="number">0</button>
</div>

With a little bit of CSS magic, we can see the component in the UI:

Figure 2.1: Phone swipe component

Figure 2.1: Phone swipe component

Meanwhile, in our swipe-unlock.component.ts file, we can reference various elements of the number pad’s UI so that we can manipulate any events that are performed on them:

@ViewChild('swipeArea')
swipeArea!: ElementRef;
@ViewChildren('one, two, three, four, five, six, seven,
              eight, nine, zero')
numbers!: QueryList<ElementRef>;

Step 2 – Identifying user touch events

What we’re interested in are the events where a user touches the screen, moves (swipes), and lifts their finger off the screen. We can create those streams of events like so:

const touchStart$ = fromEvent<TouchEvent>(
    this.swipeArea.nativeElement,
    'touchstart'
);
const touchMove$ = fromEvent<TouchEvent>(
    this.swipeArea.nativeElement,
    'touchmove'
);
const touchEnd$ = fromEvent<TouchEvent>(
    this.swipeArea.nativeElement,
    'touchend'
);

From here, we can react to these events and figure out the coordinates of a touch event, check if it’s intersecting with the number pad area, and highlight it in the UI:

const swipe$ = touchStart$.pipe(
    switchMap(() =>
        touchMove$.pipe(
            takeUntil(touchEnd$),
            map((touchMove) => ({
                x: touchMove.touches[0].clientX,
                y: touchMove.touches[0].clientY,
            }))
        )
    ),
);

Now, when we subscribe to those swipe coordinates, we can perform the required actions in sequence, such as selecting the number pad and creating a dot trail:

swipe$.pipe(
    tap((dot) => this.selectNumber(dot)),
    mergeMap((dot) => this.createTrailDot(dot)),
).subscribe();

Step 3 – Marking selected number pads

After getting the coordinates from each swipe, we can easily check whether it’s intersecting the area surrounding the number pad:

private selectNumber(dot: PixelCoordinates): void {
    this.numbersElement.forEach((number) => {
        if (
        dot.y > number.getBoundingClientRect().top &&
        dot.y < number.getBoundingClientRect().bottom &&
        dot.x > number.getBoundingClientRect().left &&
        dot.x < number.getBoundingClientRect().right
      ) {
            number.classList.add('selected');
            this.patternAttempt.push(parseInt(
                number.innerText)  
            );
        }
    });
}

By adding a selected class to each intersecting element, we can visually represent the selected number pads:

Figure 2.2: Marking the selected number pads

Figure 2.2: Marking the selected number pads

Step 4 – Creating a trail

With the help of the mergeMap operator, we can assemble all swipe events and their coordinates, create a dot in the DOM representing the trail of user action, and, after a certain delay, remove the trail from the DOM. Additionally, a nice performance consideration might be grouping many swipe events into one buffer. We can do this by using bufferCount, an operator that helps us to ensure optimal memory usage and computational efficiency:

private createTrailDot(
    dotCoordinates: PixelCoordinates
): Observable<string[]> {
    const dot = document.createElement('div');
    dot.classList.add('trail-dot');
    dot.style.left = `${dotCoordinates.x}px`;
    dot.style.top = `${dotCoordinates.y}px`;
    this.swipeArea.nativeElement.appendChild(dot);
    return of('').pipe(
        delay(1000),
        bufferCount(100, 50),
        finalize(() => dot.remove())
    );
}

Now, in our browser’s Dev Tools, we can inspect the creation of the trail by looking at the DOM:

Figure 2.3: Swipe trail

Figure 2.3: Swipe trail

Step 5 – Checking the result

Finally, at the end of the stream in the showMessage method, we must check whether the patternAttempt array, which was filled with each selected number pad, matches our pattern for unlocking the phone, which is 1 2 5 8 7.

Pattern matching

Since this is pattern matching and not exact password matching, the phone can be unlocked by inputting those buttons in any order, so long as those numbers in the pattern are included.

See also

  • The fromEvent function: https://rxjs.dev/api/index/function/fromEvent
  • The switchMap operator: https://rxjs.dev/api/operators/switchMap
  • The takeUntil operator: https://rxjs.dev/api/operators/takeUntil
  • The finalize operator: https://rxjs.dev/api/operators/finalize
  • The mergeMap operator: https://rxjs.dev/api/operators/mergeMap
  • The bufferCount operator: https://rxjs.dev/api/operators/bufferCount

Learning indications with the progress bar

Providing feedback to the user while performing actions when using web applications is one of the key aspects of a good user experience. A component like this helps users understand how long they need to wait and reduces uncertainty if the system is working. Progress bars can be also useful for gamification purposes, to make the overall UX more engaging and motivating.

How to do it…

In this recipe, we’ll simulate upload progress to the backend API by implementing a progress indicator that produces a random progress percentage until we get a response. If we still haven’t received a response after we get to the very end of the progress bar, we’ll set its progress to 95% and wait for the request to be completed.

Step 1 – Creating a progress loading stream

Inside our recipes.service.ts service, we’ll start a stream of random numbers at a given interval. This will be stopped after we get a response from the backend:

private complete$ = new Subject<void>();
private randomProgress$ = interval(800).pipe(
    map(() => Number((Math.random() * 25 + 5))), 
    scan((acc, curr) =>
        +Math.min(acc + curr, 95).toFixed(2), 0),
    takeUntil(this.complete$)
);

With the help of the scan operator, we can decide whether we should produce the next increment of a progress percentage or whether we shouldn’t go over 95%.

Step 2 – Merging progress and request streams

Now, we can combine the randomProgress$ stream with the HTTP request and notify the progress indicator component whenever we get either random progress or complete the request:

postRecipe(recipe: Recipe): Observable<number> {
    return merge(
        this.randomProgress$,
        this.httpClient.post<Recipe>(
            '/api/recipes',
            recipe
        ).pipe(
            map(() => 100),
            catchError(() => of(-1)),
            finalize(() => this.unsubscribe$.next())
        )
    )
}

Once we call the postRecipe service method inside a component, we can track the request progress:

Figure 2.4: Progress indicator

Figure 2.4: Progress indicator

See also

Streaming image loading seamlessly with Progressive Image

In the modern web, we must handle resources that are MBs in size. One such resource is images. Large images can harm performance since they have slower load times, something that could lead to a negative user experience and frustration. To address these issues, one of the common patterns to use is the LowQualityImagePlaceholder pattern, also known as Progressive Image, where we load an image in stages. First, we show the lightweight version of an image (placeholder image). Then, in the background, we load the original image.

How to do it…

In this recipe, we’ll learn how to handle the Progressive Image pattern with ease with the help of RxJS magic.

Step 1 – Defining image sources

Inside our pro-img.component.ts file, we must define paths to our local image and a placeholder/blurry version of the same image from our assets folder:

src = 'image.jpg';
placeholderSrc = 'blurry-image.jpeg';
const img = new Image();
img.src = this.src;
const placeholderImg = new Image();
placeholderImg.src = this.placeholderSrc;

Step 2 – Creating a progress stream

While the image is loading, every 100 milliseconds, we’ll increase the progress percentage, until the load event is triggered. This indicates that the image has been fully loaded. If an error occurs, we’ll say that the progress is at –1:

const loadProgress$ = timer(0, 100);
const loadComplete$ = fromEvent(img, 'load')
    .pipe(map(() => 100));
const loadError$ = fromEvent(img, 'error')
    .pipe(map(() => -1));

Now, we can merge these load events and stream them into the Progressive Image load:

loadingProgress$ = new BehaviorSubject<number>(0);
this.imageSrc$ = merge(loadProgress$, loadComplete$,loadError$).pipe(
    tap((progress) => this.loadingProgress$.next(progress)),
    map((progress) => (progress === 100 ?img.src :placeholderImg.src)),
    startWith(placeholderImg.src),
    takeWhile((src) => src === placeholderImg.src, true),
    catchError(() => of(placeholderImg.src)),
    shareReplay({ bufferSize: 1, refCount: true })
);

We’ll use startWith on the placeholder image and show it immediately in the UI while continuously tracking the progress of the original image load. Once we get 100%, we’ll replace the placeholder image source with the original image.

Step 3 – Subscribing to the image stream in the template

Meanwhile, in the component template, pro-img.component.html, we can subscribe to the progress that’s been made while the image is loading in the background:

<div class="pro-img-container">
    @if ((loadingProgress$ | async) !== 100) {
        <div class="progress">
        {{ loadingProgress$ | async }}%
        </div>
    }
    <img
    [src]="imageSrc$ | async"
    alt="Progressive image"
    class="pro-img"
    >
</div>

Finally, if we open our browser, we may see this behavior in action:

Figure 2.5: Progressive Image
Figure 2.5: Progressive Image

Figure 2.5: Progressive Image

Common gotcha

In this recipe, for simplicity, we’ve chosen to artificially increase the download progress of an image. The obvious drawback is that we don’t get the actual progress of the image download. There’s a way to achieve this effect: by converting the request of an image’s responseType into a blob. More details can be found here: https://stackoverflow.com/questions/14218607/javascript-loading-progress-of-an-image.

See also

  • The Ultimate LQIP Technique, by Harry Roberts: https://csswizardry.com/2023/09/the-ultimate-lqip-lcp-technique/
  • The HTML load event: https://developer.mozilla.org/en-US/docs/Web/API/Window/load_event
  • The takeWhile operator: https://rxjs.dev/api/operators/takeWhile
  • The startWith operator: https://rxjs.dev/api/operators/startWith

Optimizing loading tab content

When tabs contain complex data or media-rich content, it’s beneficial to load the content of tabs lazily. By doing so, we aim to minimize initial page load times, conserve bandwidth, and ensure a smooth and responsive interface. So, let’s create a component like that.

How to do it…

In this recipe, we’ll have a simple tab group of two tabs. Only when a tab is selected will we lazy-load the component representing the contents of that tab. Each tab is represented in the URL, so whenever we change tabs, we’re navigating to a separate page.

Step 1 – Defining a tab group and an active tab

In our tabs.component.html file, we’ll use the Angular Material tab to represent a tab group in the UI:

<mat-tab-group
    [selectedIndex]="(activeTab$ | async)?.index"
    (selectedTabChange)="selectTab($event)"
>
    <ng-container *ngFor="let tab of tabs">
        <mat-tab [label]="tab.label"></mat-tab>
    </ng-container>
</mat-tab-group>

Now, inside tabs.component.ts, we need to define the activeTab and loading states, as well as the content of a tab stream that we can subscribe to:

activeTab$ = new BehaviorSubject<TabConfig | null>(null);
activeTabContent$!: Observable<
    typeof TabContentComponent |
    typeof TabContent2Component |
    null
>;
loadingTab$ = new BehaviorSubject<boolean>(false);

Now, we can hook into Angular Router events, filter events when navigation ends, and, based on an active URL, mark the corresponding tab as active:

this.router.events.pipe(
    filter((event) => event instanceof NavigationEnd),
    takeUntil(this.destroy$)
).subscribe({
    next: () => {
        const activeTab = this.tabs.find(
            (tab) => tab.route === this.router.url.slice(1)
        );
    this.activeTab$.next(activeTab || null);
    },
});

Step 2 – Loading tab content

Since we know which tab is active, we can start loading the content of that tab:

private loadTabContent(tab: TabConfig) {
    const content$ = tab.route === 'tab1'
    ? of(TabContentComponent)
    : of(TabContent2Component);
    return content$.pipe(delay(1000));
}
this.activeTabContent$ = this.activeTab$.pipe(
    tap(() => this.loadingTab$.next(true)),
    switchMap((tab) =>
        this.loadTabContent(tab!).pipe(
            startWith(null),
            catchError((error) => {
                this.errors$.next(error);
            return of(null);
            }),
        finalize(() => this.loadingTab$.next(false))
        )
    ),
    shareReplay({ bufferSize: 1, refCount: true })
);

Inside the loadTabContent method, we’ll create an Observable out of the Angular component that’s matched based on the current route. Once we’ve done this, we’re ready to stream into the tab content whenever the active tab changes. We can do this by starting the loading state, switching to the stream that’s loading content, and resetting the loading state once the content has arrived.

Now, all we need to do is represent the content in the UI. Back in our tabs.component.html file, we can simply add the following code:

@if (loadingTab$ | async) {
    <p>Loading...</p>
}
<ng-container
    *ngComponentOutlet="activeTabContent$ | async"
></ng-container>

Now, by going to our browser, we’ll see that the content of a tab will only be loaded when we click on that specific tab:

Figure 2.6: Loading tabs
Figure 2.6: Loading tabs

Figure 2.6: Loading tabs

See also

  • The of function: https://rxjs.dev/api/index/function/of
  • The startWith operator: https://rxjs.dev/api/operators/startWith
  • Angular’s Router’ NavigationEnd event: https://angular.dev/api/router/NavigationEnd
  • The Angular Material tab component: https://material.angular.io/components/tabs/overview

Reacting to drag-and-drop events

Creating a drag-and-drop component for file uploads is quite a common task for a web developer. If you’ve ever worked on such a component, you may already know that it isn’t a trivial task and that there’s a lot of hidden complexity behind a component like this. Luckily for us, we have RxJS to help us streamline the experience of reacting to drag-and-drop events in a reactive and declarative way.

Getting ready

In this recipe, to provide support for tracking image upload progress, we need to run a small Node.js server application located in the server folder. We can run this server application by using the following command:

node index.js

After that, we’re ready to go to the client folder and dive into the reactive drag-and-drop component.

How to do it…

In this recipe, we’ll define a drag-and-drop area for .png images. Then, we’ll add support for multiple uploads to be made at the same time, show the upload progress of each image, and display error messages if the format of the image isn’t correct. We’ll also implement a retry mechanism in case a file upload fails over the network.

Step 1 – Defining a dropzone

In our dnd-file-upload.component.html file, we must place markup for the dropzone area:

<div #dropzoneElement class="drop-zone-element">
    <p>Drag and drop png image into the area below</p>
</div>

After getting the dropzoneElement reference with @ViewChild(), we can start reacting to the drag-and-drop events in the dropzone area:

@ViewChild('dropzoneElement') dropzoneElement!: ElementRef;
ngAfterViewInit(): void {
    const dropzone = this.dropzoneElement.nativeElement;
    const dragenter$ = fromEvent<DragEvent>(
        dropzone,
        'dragenter'
    );
    const dragover$ = fromEvent<DragEvent>(
        dropzone,
        'dragover'
    ).pipe(
        tap((event: DragEvent) => {
            event.preventDefault();
            event.dataTransfer!.dropEffect = 'copy';
            (event.target as Element).classList.add('dragover');
        })
    );
    const dragleave$ = fromEvent<DragEvent>(
        dropzone,
        'dragleave'
    ).pipe(
        tap((event: DragEvent) => {
            (event.target as Element).classList.remove('dragover');
        })
    );
    const drop$ = fromEvent<DragEvent>(
        dropzone,
        'drop'
    ).pipe(
        tap((event: DragEvent) => {
            (event.target as Element).classList.remove('dragover');
        })
    );
    const droppable$ = merge(
        dragenter$.pipe(map(() => true)),
        dragover$.pipe(map(() => true)),
        dragleave$.pipe(map(() => false))
    );
}

While creating these events, we can track when the file(s) have entered the dropzone and when they’re leaving. Based on this, we can style the component by adding the corresponding classes. We’ve also defined all droppable even so that we know when to stop reacting to the stream of new images that’s being dragged over.

Step 2 – Validating files

Now, we can hook into a stream of drop events and validate the format of each image; if the format is OK, we can start uploading each image to the backend API:

drop$.pipe(
    tap((event) => event.preventDefault()),
    switchMap((event: DragEvent) => {
        const files$ = from(Array.from(
            event.dataTransfer!.files));
        return this.fileUploadService.validateFiles$(
            files$);
    }),
  ...the rest of the stream

Back in our FileUploadService service, we have a validation method that checks whether we’ve uploaded a .png image:

validateFiles$(files: Observable<File>): Observable<{
    valid: boolean,
    file: FileWithProgress
}> {
    return files.pipe(
        map((file File) => {
            const newFile: FileWithProgress = new File(
                [file],
                file.name,
                { type: file.type }
            );
            if (file.type === 'image/png') {
                newFile.progress = 0;
            } else {
                newFile.error = 'Invalid file type';
            }
        return newFile;
        }),
        map((file: FileWithProgress) => {
            return of({
                valid: !file.error,
                file
            });
        }),
        mergeAll()
    );
}

Here, we check the file type. If it’s expected, we set the progress to 0 and start the upload. Otherwise, we set the error message for that specific file upload.

Step 3 – Uploading files and tracking progress

Once we’ve validated each file, we can start upload them to the backend:

drop$.pipe(
    // validation steps from Step 1
    map((file: FileWithProgress) =>
        this.fileUploadService.handleFileValidation(file)
    ),
    mergeAll(),
    takeUntil(droppable$
        .pipe(filter((isDroppable) => !isDroppable))
    ),
    repeat()
)
handleFileValidation$(file: FileWithProgress): 
    Observable<FileWithProgress | never> {
        if (!file.valid) {
            this._snackBar.open(
                `Invalid file ${file.name} upload.`,
                'Close',
                { duration: 4000 }
            );
        return EMPTY;
    }
    return this.fileUploadService
        .uploadFileWithProgress$(file);
}

If the file is invalid, we’ll immediately return that file and show the error in the UI:

Figure 2.7: Invalid file format upload

Figure 2.7: Invalid file format upload

If it’s a valid file upload, then we initiate an upload request to our API. In Angular, if we want to track the actual progress of a request, there are a few things we must do:

  1. We need to send the request payload as FormData.
  2. We need to set responseType to 'blob'.
  3. We need to set the reportProgress flag to true.

After applying all these steps, our uploadFiles$ method should look like this:

uploadFile$(file: File): Observable<number> {
    const formData = new FormData();
    formData.append('upload', file);
    const req = new HttpRequest(
        'POST', '/api/recipes/upload', formData, {
            reportProgress: true,
            responseType: 'blob'
        }
    );
    return this.httpClient.request(req).pipe(
        map((event: HttpEvent<Blob>) =>
            this.getFileUploadProgress(event)),
        filter(progress => progress < 100),
    );
}

Now, when we send this request, we’ll get a series of HTTP events that we can react to. If we check the getFileUploadProgress method, we’ll see this in action:

getFileUploadProgress(event: HttpEvent<Blob>): number {
    const { type } = event;
    if (type === HttpEventType.Sent) {
        return 0;
    }
    if (type === HttpEventType.UploadProgress) {
        const percentDone = Math.round(
            100 * event.loaded / event.total!);
        return percentDone;
    }
    if (type === HttpEventType.Response) {
        return 100;
    }
    return 0;
}

With this approach, we know the exact progress of the file upload due to the UploadProgress event.

Finally, we can call the uploadFileWithProgress$ method from our service and return each file with progress information attached to each corresponding file:

uploadFileWithProgress$(file: FileWithProgress):    Observable<FileWithProgress> {
    return this.uploadFile$(file).pipe(
        map((progress: number) =>
            this.createFileWithProgress(file, progress)),
        endWith(this.createFileWithProgress(file, 100))
    );
}

After emitting a progress value, we’ll return the file with information attached about its progress so that we can display it in the UI.

Step 4 – Showing file uploads in the UI

Finally, once we subscribe to this whole stream of file upload events inside of our component, we can show the list of all the files that are being uploaded with corresponding progress bars. This also allows us to show an error message if an error has occurred:

drop$.pipe(
    // validation steps from Step 1
    // file upload steps from Step 2
).subscribe({
    next: (file) => {
        if (file.valid) {
            this.validFiles.set(file.name, file);
            return;
        }
        if (!file.valid) {
            this._snackBar.open(
                'Invalid file upload.',
                'Close',
            {}
            );
        }
    }
});

Once we open our browser and drag multiple valid .png images, we can handle those uploads concurrently and observe their progress:

Figure 2.8: A reactive drag-and-drop file upload

Figure 2.8: A reactive drag-and-drop file upload

Step 5 – Handling file upload errors

Imagine that, in the middle of our image upload, the network fails. One of the key aspects of a component like this is that it must be resilient to these kinds of errors and provide a recovery or retry mechanism. We can do this by catching that network error in the file upload stream and showing a retry button in the UI next to the failed upload. We can extend our service method by adding an error catch mechanism:

uploadFileWithProgress$(file: FileWithProgress): Observable<FileWithProgress> {
    return this.uploadFile$(file).pipe(
        map((progress: number) =>
            this.createFileWithProgress(file, progress)),
        endWith(this.createFileWithProgress(file, 100)),
        catchError(() => {
            const newFile: FileWithProgress =
                this.createFileWithProgress(
                    file,
                    -1,
                    'Upload failed'
                );
            return of(newFile);
        })
    );
}

Back in our component template, dnd-file-upload.component.html, we can add a retry button if the file’s upload progress is at –1, meaning that it failed previously:

@if (file.value.progress !== -1) {
    {{ file.value.progress }}%
} @else {
    <button
        mat-icon-button
        (click)="retryUpload(file.value)"
        >
        <mat-icon aria-hidden="false" fontIcon="redo">
        </mat-icon>
    </button>
}
retryUpload(file: FileWithProgress): void {
    this.recipeService.uploadFileWithProgress$(
        file).subscribe({ next: (file: FileWithProgress) =>
            this.validFiles.set(file.name, file),
            error: (err) => console.error(err),
        });
}

If we open our browser, if an upload error has occurred, we may notice the retry button in the UI. If the network recovers, we can trigger another upload request for the failed uploads:

Figure 2.9: Retry on file upload

Figure 2.9: Retry on file upload

See also

  • The HTML input file: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file
  • The interval function: https://rxjs.dev/api/index/function/interval
  • The repeat operator: https://rxjs.dev/api/operators/repeat
  • The scan operator: https://rxjs.dev/api/operators/scan
  • The finalize operator: https://rxjs.dev/api/operators/finalize
  • The merge operator: https://rxjs.dev/api/operators/merge
  • The mergeAll operator: https://rxjs.dev/api/operators/mergeAll
  • The endWith operator: https://rxjs.dev/api/operators/endWith

Crafting your perfect audio player using flexible RxJS controls

Everybody likes music. Whether you use Spotify, Deezer, YouTube, or something else to listen to your favorite jams, having control over your playlist with a sophisticated audio player is one of the essential conditions for providing an awesome user experience. In this recipe, we’ll create a lightweight RxJS audio player with reactive controls for playing and pausing songs, controlling volume, as well as skipping to the next song in the playlist.

How to do it…

The essential thing to understand in this recipe is the native HTMLAudioElement and, based on that, which events are the most important to react to.

Step 1 – Creating audio player events

In our audio-player.component.html file, we must implement markup for the audio player:

<audio #audio></audio>

Concerning that audio HTML element, in the component audio-player.component.ts file, we’ll define all the key events for that element:

@ViewChild('audio') audioElement!:
    ElementRef<HTMLAudioElement>;
ngAfterViewInit(): void {
    const audio = this.audioElement.nativeElement;
    const duration$ = fromEvent(audio,  
        'loadedmetadata').pipe(map(() => (
            { duration: audio.duration }))
    );
    const playPauseClick$ = fromEvent(audio, 'play').pipe(
        map(() => ({ isPlaying: true }))
    );
    const pauseClick$ = fromEvent(audio, 'pause').pipe(
        map(() => ({ isPlaying: false }))
    );
    const volumeChange$ = fromEvent(audio,
        'volumechange').pipe(
             map(() => ({ volume: audio.volume })),
    );
    const time$ = fromEvent(audio, 'timeupdate').pipe(
        map(() => ({ time: audio.currentTime }))
    );
    const error$ = fromEvent(audio, 'error');
}

Using the audio element, we can react to play, pause, volumechange, and timeupdate events, as well as metadata that holds information about the duration value of a song. Also, in case network interruptions occur when we fetch the audio file or corrupted audio files, we can subscribe to the error event from the audio element.

Now, we can combine all those events and hold the state of a song in a centralized place:

merge(
    duration$,
    playPauseClick$,
    pauseClick$,
    volumeChange$
).subscribe((state) =>
    this.audioService.updateState(state));

Step 2 – Managing song state

In our audio.service.ts file, we’ll store the state of the current song:

public audioState$ = new BehaviorSubject<AudioState>({
    isPlaying: false,
    volume: 0.5,
    currentTrackIndex: 0,
    duration: 0,
    tracks: []
});
updateState(state: Partial<AudioState>): void {
    this.audioState$.next({
     ...this.audioState$.value,
     ...state
    });
}

Now, we can subscribe to all state changes in the component and have reactive audio player controls over user actions.

Step 3 – Playing/pausing a song

Back in our audio-player.component.ts file, whenever play or pause events are being emitted, the state will update, at which point we can subscribe to the state change:

this.audioService.audioState$.subscribe(({ isPlaying }) =>
    this.isPlaying = isPlaying;
);

Now, in the audio-player.component.html file, we can present either a play or pause icon based on the following condition:

<button mat-fab class="play-pause-btn" (click)="playPause()">
    @if (isPlaying) {
        <mat-icon>pause</mat-icon>
    } @else {
        <mat-icon>play_arrow</mat-icon>
    }
</button>

We can also control the audio when playing a song:

playPause(): void {
    if (!this.isPlaying) {
        this.audioElement.nativeElement.play();
    } else {
        this.audioElement.nativeElement.pause();
    }
}

Step 4 – Controlling the song’s volume

By subscribing to the audio player state, we also have information about the volume based on the previously emitted volumechange event:

this.audioService.audioState$.subscribe(({ volume }) => {
    this.volume = volume;
});

We can represent this state in the UI like so:

<div class="volume">
    @if (volume === 0) {
        <mat-icon>volume_off</mat-icon>
    } @else {
        <mat-icon>volume_up</mat-icon>
    }
    <input
        type="range"
        [value]="volume"
        min="0"
        max="1"
        step="0.01"   
        (input)="changeVolume($event)"
    />
</div>

Now, we can emit the same event by changing the volume of the audio player by invoking the changeVolume() method:

changeVolume({ target: { value } }): void {
    this.audioElement.nativeElement.volume = value;
}

This will automatically update the volume state reactively on the audio player element.

Step 5 – Switching songs

Back in our audio.service.ts file, we’ve implemented methods for changing the current song index in the list of tracks:

previousSong(): void {
    let prevIndex =
        this.audioState$.value.currentTrackIndex - 1;
    const tracks = this.audioState$.value.tracks;
    if (prevIndex < 0) {
        prevIndex = tracks.length - 1; // Loop back to the
                                       // end
    }
    this.updateState({
        isPlaying: false,
        currentTrackIndex: prevIndex
    });
}
nextSong(): void {
    let nextIndex =
        this.audioState$.value.currentTrackIndex + 1;
    const tracks = this.audioState$.value.tracks;
    if (nextIndex >= tracks.length) {
        nextIndex = 0; // Loop back to the beginning
    }
    this.updateState({
        isPlaying: false,
        currentTrackIndex: nextIndex
    });
}

Also, when we come to the end of the list, we’ll loop to the beginning of the playlist.

Inside the audio-player.component.ts component, we can subscribe to this state change and change the song using the audio element:

this.audioService.audioState$.subscribe(({
    currentTrackIndex,
    tracks
}) => {
    if (
        tracks[currentTrackIndex].title !==
            this.currentTrack.title
    ) {
        this.audioElement.nativeElement.src =
            tracks[currentTrackIndex].song;
        this.currentTrack = tracks[currentTrackIndex];
    }
});

This means that we have all the information we need about the current song, which means we can display that data in our audio-player.component.html template.

Step 6 – Skipping to the middle of a song

In our audio element, there’s a timeupdate event that lets us track and update the current time of a song:

const time$ = fromEvent(audio, 'timeupdate').pipe(
    map(() => ({ time: audio.currentTime }))
);
time$.subscribe(({ time }) => this.currentTime = time);

In the UI, we can combine this current time information with the previous song metadata, show it in a slider, and watch the song progress:

<p>{{ currentTime | time }}</p>
<audio #audio></audio>
<mat-slider [max]="duration" class="song">
    <input matSliderThumb
    [value]="currentTime"
    (dragEnd)="skip($event)"
    >
</mat-slider>
<p>{{ duration | time }}</p>

Finally, if we open our browser, we can inspect all these features and play our favorite jam:

Figure 2.10: Reactive audio player

Figure 2.10: Reactive audio player

See also

Streamlining real-time updates with RxJS-powered notifications

Notifications are one of the main ways we can prompt users about relevant events or changes within the system. By utilizing Observables and operators, RxJS provides a powerful framework for managing these asynchronous notifications efficiently and effectively.

How to do it…

In this recipe, we’ll have an array of notifications to represent incoming notifications based on a user action, store them by ID, and remove them after a certain period. We’ll also provide support to manually remove notifications from a stack.

Step 1 – Stacking incoming notifications

To streamline the stack of notifications efficiently, inside NotificationService, we’ll use BehaviorSubject to represent all the notifications that may arrive over time asynchronously. We’ll also have a Subject that triggers an event when we want to add a new notification to the stack and another for dismissal:

private notifications$ = new BehaviorSubject<Notification[]>([]);
private addNotification$ = new Subject<Notification>();
private removeNotification$ = new Subject<string>();
addNotification(notification: Notification) {
    this.addNotification$.next(notification);
}
removeNotification(id: string) {
    this.removeNotification$.next(id);
}

So, whenever there’s an ongoing request for posting new data, we’ll combine these two actions with the latest state of the notification stack with the help of the withLatestFrom operator and update its state:

get notifications(): Observable<Notification[]> {
    return merge(
        this.addNotification$,
        this.removeNotification$
    ).pipe(
    withLatestFrom(this.notifications$),
    map(([changedNotification, notifications]) => {
        if (changedNotification instanceof Object) {
            this.notifications$.next([
                ...notifications,
                changedNotification
            ]);
          } else {
              this.notifications$.next(notifications.filter   
                  (notification =>
                       notification.id !== changedNotification)
              );
          }
          return this.notifications$.value;
        })
    )
}

Based on the latest emitted value’s type, we can decide whether a new notification needs to be added or filtered from the stack.

Step 2 – Reacting to a user action and displaying notifications

In our app.component.html file, we have a simple button that will trigger a POST request to add a new random cooking recipe:

<button (click)="sendRequest()">Add recipe</button>

Clicking that button will invoke a function:

sendRequest() {
    this.recipeService.postRecipes();
}

In RecipeService, we must implement the service method for sending the request to the BE API. If we get a successful response, we’ll perform a side effect to add a notification to the stack. If we get an error, we’ll display a notification that’s of the error type:

getRecipes(): void {
    this.httpClient.get<Recipe[]>('/api/recipes').pipe(
        tap(() => {
            this.notificationService.addNotification({
                id: crypto.randomUUID(),
                message: 'Recipe added successfully.',
                type: 'success'
        });
    }),
    catchError((error) => {
        this.notificationService.addNotification({
            id: crypto.randomUUID(),
            message: 'Recipe could not be added.',
            type: 'error'
        });
        return throwError(() =>
            new Error('Recipe could not be added.'));
       }),
    ).subscribe();
}

Finally, in NotificationComponent, we can subscribe to the changes on notifications$ and display notifications:

<div class="container">
    <div
      *ngFor="let notification of notifications | async"  
      class="notification {{ notification.type }}"
    >
      {{ notification.message }}
        <mat-icon
            (click)="removeNotification(notification.id)" 
            class="close">
            Close
        </mat-icon>
    </div>
</div>

Now, when we open our browser, we’ll see incoming notifications stacked on each other:

Figure 2.11: A reactive stack of notifications

Figure 2.11: A reactive stack of notifications

Step 3 – Automatic notification dismissal

Previously, we could manually remove notifications from the stack by clicking the close button. Now, after a certain period, we want a notification to be automatically removed from the stack. Back in NotificationService, when adding a notification to the stack initially, we’ll simply define a timer, after which we’ll call the removeNotification method:

addNotification(
    notification: Notification,
    timeout = 5000
) {
    this.addNotification$.next(notification);
    timer(timeout).subscribe(() =>
        this.removeNotification(notification.id));
}

See also

Fetching data with the Infinite Scroll Timeline component

Imagine going through your favorite cooking web application and getting the latest updates on delicious new recipes. To show this latest news, one of the common UX patterns is to show this recipe news in a timeline component (such as Facebook’s news feed). While you scroll, if there are new recipes, you’ll be updated that there are fresh new recipes so that you can scroll back to the top and start over.

How to do it…

In this recipe, we’re going to build a timeline component that shows the list of your favorite latest cooking recipes. Since there are a lot of delicious recipes out there, this would be a huge list to fetch initially. To increase the performance of the application and to improve the general UX, we can implement an infinite scroll list so that once the user scrolls to the end of a list of 5 initial recipes, we can get a set of 5 new recipes. After some time, we can send a new request to check whether there are new recipes and refresh our timeline of recipe news.

Step 1 – Detecting the end of a list

In our RecipesList component, we’ll create a stream of scroll events. On each emission, we’ll check whether we’re near the end of the list in the UI based on a certain threshold:

private isNearBottom(): boolean {
    const threshold = 100; // Pixels from bottom
    const position = window.innerHeight + window.scrollY;
    const height = document.documentElement.scrollHeight;
    return position > height - threshold;
}
const isNearBottom$ = fromEvent(window, 'scroll').pipe(
    startWith(null),
    auditTime(10), // Prevent excessive event triggering
    observeOn(animationFrameScheduler),
    map(() => this.isNearBottom()),
    distinctUntilChanged(), // Emit only when near-bottom 
                             //state changes
)

As you can imagine, with the scroll event emissions, there’s the potential for performance bottlenecks. We can limit the number of scroll events that are processed by the stream using the auditTime operator. This is especially useful since we want to ensure that we are always processing the latest scroll event, and auditTime will always emit the most recent value within the specified time frame. Also, with observeOn(animationFrameScheduler), we can schedule tasks to be executed just before the browser’s next repaint. This can be beneficial for animations or any updates that cause a repaint as it can help to prevent jank and make the application feel smoother.

auditTime versus throttleTime

You might be wondering why we used auditTime in our scroll stream and not throttleTime. The key difference between these two operators is that auditTime emits the last value in a time window, whereas throttleTime emits the first value in a time window. Common use cases for throttleTime might include rate-limiting API calls, handling button clicks to prevent accidental double clicks, and controlling the frequency of animations.

Once we know we’re getting near the end of a list, we can trigger a loading state and the next request with a new set of data.

Step 2 – Controlling the next page and loading the state of the list

At the top of our RecipesList component, we’ll define the necessary states to control the whole flow and know when we require the next page, when to show the loader, and when we’ve reached the end of the list:

private page = 0;
public loading$ = new BehaviorSubject<boolean>(false);
public noMoreData$ = new Subject<void>();
private destroy$ = new Subject<void>();

Now, we can continue our isNearBottom$ stream, react to the next page, and specify when to show the loader:

isNearBottom$.pipe(  
    filter((isNearBottom) =>
        isNearBottom && !this.loading$.value),
    tap(() => this.loading$.next(true)),
    switchMap(() =>
        this.recipesService.getRecipes(++this.page)
        .pipe(
            tap((recipes) => {
                if (recipes.length === 0)
                this.noMoreData$.next();
            }),
            finalize(() => this.loading$.next(false))
        )
    ),
    takeUntil(merge(this.destroy$, this.noMoreData$))
    )
    .subscribe((recipes) => (
        this.recipes = [...this.recipes, ...recipes])
    );
}

Here’s a breakdown of what we’ve done:

  1. First, we check whether we’re near the bottom of the page or whether there’s already an ongoing request.
  2. We start a new request by showing a loading spinner.
  3. We send a new request with the next page as a parameter.
  4. When we get a successful response, we can check whether there’s no more data or we can continue scrolling down the timeline.
  5. Once the stream has finished, we remove the loading spinner:
Figure 2.12: Reactive infinite scroll

Figure 2.12: Reactive infinite scroll

Step 3 – Checking for new recipes

In our recipes.service.ts file, we’ve implemented a method that will check whether there are new recipes periodically and whether we should scroll to the top of the timeline and refresh it with new data:

checkNumberOfNewRecipes(): Observable<number> {
    return interval(10000).pipe(
        switchMap(() =>
            this.httpClient.get<number>(
                '/api/new-recipes'))
    );
}

Once we receive several new recipes, we can subscribe to that information inside NewRecipesComponent and display it in the UI:

Figure 2.13: Reactive timeline updates

Figure 2.13: Reactive timeline updates

Now, once we click the 2 new recipes button, we can scroll to the top of the timeline and get the newest data.

See also

Left arrow icon Right arrow icon
Download code icon Download Code

Key benefits

  • Master RxJS observables, operators, and subjects to improve your reactive programming skills
  • Explore advanced concepts like error handling, state management, PWA, real-time, and event-driven systems
  • Enhance reactive skills with modern web programming techniques, best practices, and real-world examples
  • Purchase of the print or Kindle book includes a free PDF eBook

Description

Building modern web applications that are responsive and resilient is essential in this rapidly evolving digital world. Imagine effortlessly managing complex data streams and creating seamless user experiences—this book helps you do just that by adopting RxJS to boost your development skills and transform your approach to reactive programming. Written by a seasoned software engineer and consultant with a decade of industry experience, this book equips you to harness the power of RxJS techniques, patterns, and operators tailored for real-world scenarios. Each chapter is filled with practical recipes designed to tackle a variety of challenges, from managing side effects and ensuring error resiliency in client applications to developing real-time chat applications and event-driven microservices. You’ll learn how to integrate RxJS with popular frameworks, such as Angular and NestJS, gaining insights into modern web development practices that enhance performance and interactivity. By the end of this book, you’ll have mastered reactive programming principles, the RxJS library, and working with Observables, while crafting code that reacts to changes in data and events in a declarative and asynchronous way.

Who is this book for?

This book is ideal for intermediate-to-advanced JavaScript developers who want to adopt reactive programming principles using RxJS. Whether you’re working with Angular or NestJS, you’ll find recipes and real-world examples that help you leverage RxJS for managing asynchronous operations and reactive data flows across both your frontend and backend.

What you will learn

  • Manage error handling, side effects, and event orchestration in your Angular and NestJS applications
  • Use RxJS to build stunning, interactive user interfaces with Angular
  • Apply Angular testing strategies to ensure the reliability of your RxJS-powered applications
  • Optimize the performance of RxJS streams
  • Enhance progressive web app experiences with RxJS and Angular
  • Apply RxJS principles to build state management in Angular
  • Craft reactive and event-driven microservices in NestJS

Product Details

Country selected
Publication date, Length, Edition, Language, ISBN-13
Publication date : Mar 28, 2025
Length: 310 pages
Edition : 1st
Language : English
ISBN-13 : 9781788625319
Languages :
Tools :

What do you get with eBook?

Product feature icon Instant access to your Digital eBook purchase
Product feature icon Download this book in EPUB and PDF formats
Product feature icon Access this title in our online reader with advanced features
Product feature icon DRM FREE - Read whenever, wherever and however you want
OR
Modal Close icon
Payment Processing...
tick Completed

Billing Address

Product Details

Publication date : Mar 28, 2025
Length: 310 pages
Edition : 1st
Language : English
ISBN-13 : 9781788625319
Languages :
Tools :

Packt Subscriptions

See our plans and pricing
Modal Close icon
€18.99 billed monthly
Feature tick icon Unlimited access to Packt's library of 7,000+ practical books and videos
Feature tick icon Constantly refreshed with 50+ new titles a month
Feature tick icon Exclusive Early access to books as they're written
Feature tick icon Solve problems while you work with advanced search and reference features
Feature tick icon Offline reading on the mobile app
Feature tick icon Simple pricing, no contract
€189.99 billed annually
Feature tick icon Unlimited access to Packt's library of 7,000+ practical books and videos
Feature tick icon Constantly refreshed with 50+ new titles a month
Feature tick icon Exclusive Early access to books as they're written
Feature tick icon Solve problems while you work with advanced search and reference features
Feature tick icon Offline reading on the mobile app
Feature tick icon Choose a DRM-free eBook or Video every month to keep
Feature tick icon PLUS own as many other DRM-free eBooks or Videos as you like for just $5 each
Feature tick icon Exclusive print discounts
€264.99 billed in 18 months
Feature tick icon Unlimited access to Packt's library of 7,000+ practical books and videos
Feature tick icon Constantly refreshed with 50+ new titles a month
Feature tick icon Exclusive Early access to books as they're written
Feature tick icon Solve problems while you work with advanced search and reference features
Feature tick icon Offline reading on the mobile app
Feature tick icon Choose a DRM-free eBook or Video every month to keep
Feature tick icon PLUS own as many other DRM-free eBooks or Videos as you like for just $5 each
Feature tick icon Exclusive print discounts

Table of Contents

12 Chapters
Handling Errors and Side Effects in RxJS Chevron down icon Chevron up icon
Building User Interfaces with RxJS Chevron down icon Chevron up icon
Understanding Reactive Animation Systems with RxJS Chevron down icon Chevron up icon
Testing RxJS Applications Chevron down icon Chevron up icon
Performance Optimizations with RxJS Chevron down icon Chevron up icon
Building Reactive State Management Systems with RxJS Chevron down icon Chevron up icon
Building Progressive Web Apps with RxJS Chevron down icon Chevron up icon
Building Offline-First Applications with RxJS Chevron down icon Chevron up icon
Going Real-Time with RxJS Chevron down icon Chevron up icon
Building Reactive NestJS Microservices with RxJS Chevron down icon Chevron up icon
Index Chevron down icon Chevron up icon
Other Books You May Enjoy Chevron down icon Chevron up icon
Get free access to Packt library with over 7500+ books and video courses for 7 days!
Start Free Trial

FAQs

How do I buy and download an eBook? Chevron down icon Chevron up icon

Where there is an eBook version of a title available, you can buy it from the book details for that title. Add either the standalone eBook or the eBook and print book bundle to your shopping cart. Your eBook will show in your cart as a product on its own. After completing checkout and payment in the normal way, you will receive your receipt on the screen containing a link to a personalised PDF download file. This link will remain active for 30 days. You can download backup copies of the file by logging in to your account at any time.

If you already have Adobe reader installed, then clicking on the link will download and open the PDF file directly. If you don't, then save the PDF file on your machine and download the Reader to view it.

Please Note: Packt eBooks are non-returnable and non-refundable.

Packt eBook and Licensing When you buy an eBook from Packt Publishing, completing your purchase means you accept the terms of our licence agreement. Please read the full text of the agreement. In it we have tried to balance the need for the ebook to be usable for you the reader with our needs to protect the rights of us as Publishers and of our authors. In summary, the agreement says:

  • You may make copies of your eBook for your own use onto any machine
  • You may not pass copies of the eBook on to anyone else
How can I make a purchase on your website? Chevron down icon Chevron up icon

If you want to purchase a video course, eBook or Bundle (Print+eBook) please follow below steps:

  1. Register on our website using your email address and the password.
  2. Search for the title by name or ISBN using the search option.
  3. Select the title you want to purchase.
  4. Choose the format you wish to purchase the title in; if you order the Print Book, you get a free eBook copy of the same title. 
  5. Proceed with the checkout process (payment to be made using Credit Card, Debit Cart, or PayPal)
Where can I access support around an eBook? Chevron down icon Chevron up icon
  • If you experience a problem with using or installing Adobe Reader, the contact Adobe directly.
  • To view the errata for the book, see www.packtpub.com/support and view the pages for the title you have.
  • To view your account details or to download a new copy of the book go to www.packtpub.com/account
  • To contact us directly if a problem is not resolved, use www.packtpub.com/contact-us
What eBook formats do Packt support? Chevron down icon Chevron up icon

Our eBooks are currently available in a variety of formats such as PDF and ePubs. In the future, this may well change with trends and development in technology, but please note that our PDFs are not Adobe eBook Reader format, which has greater restrictions on security.

You will need to use Adobe Reader v9 or later in order to read Packt's PDF eBooks.

What are the benefits of eBooks? Chevron down icon Chevron up icon
  • You can get the information you need immediately
  • You can easily take them with you on a laptop
  • You can download them an unlimited number of times
  • You can print them out
  • They are copy-paste enabled
  • They are searchable
  • There is no password protection
  • They are lower price than print
  • They save resources and space
What is an eBook? Chevron down icon Chevron up icon

Packt eBooks are a complete electronic version of the print edition, available in PDF and ePub formats. Every piece of content down to the page numbering is the same. Because we save the costs of printing and shipping the book to you, we are able to offer eBooks at a lower cost than print editions.

When you have purchased an eBook, simply login to your account and click on the link in Your Download Area. We recommend you saving the file to your hard drive before opening it.

For optimal viewing of our eBooks, we recommend you download and install the free Adobe Reader version 9.