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
$19.99 per month
Paperback Mar 2025 318 pages 1st Edition
eBook
$31.99 $35.99
Paperback
$44.99
Subscription
Free Trial
Renews at $19.99p/m
Arrow left icon
Profile Icon Nikola Mitrović
Arrow right icon
$19.99 per month
Paperback Mar 2025 318 pages 1st Edition
eBook
$31.99 $35.99
Paperback
$44.99
Subscription
Free Trial
Renews at $19.99p/m
eBook
$31.99 $35.99
Paperback
$44.99
Subscription
Free Trial
Renews at $19.99p/m

What do you get with a Packt Subscription?

Free for first 7 days. $19.99 p/m after that. Cancel any time!
Product feature icon Unlimited ad-free access to the largest independent learning library in tech. Access this title and thousands more!
Product feature icon 50+ new titles added per month, including many first-to-market concepts and exclusive early access to books as they are being written.
Product feature icon Innovative learning tools, including AI book assistants, code context explainers, and text-to-speech.
Product feature icon Thousands of reference materials covering every tech concept you need to stay up to date.
Subscribe now
View plans & pricing
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

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

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

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

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

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

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

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

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

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

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

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

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

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

Get This Book’s PDF Version and Exclusive Extras

Scan the QR code (or go to packtpub.com/unlock). Search for this book by name, confirm the edition, and then follow the steps on the page.

Note: Keep your invoice handy. Purchases made directly from Packt don’t require one.

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: 318 pages
Edition : 1st
Language : English
ISBN-13 : 9781788624053
Languages :
Tools :

What do you get with a Packt Subscription?

Free for first 7 days. $19.99 p/m after that. Cancel any time!
Product feature icon Unlimited ad-free access to the largest independent learning library in tech. Access this title and thousands more!
Product feature icon 50+ new titles added per month, including many first-to-market concepts and exclusive early access to books as they are being written.
Product feature icon Innovative learning tools, including AI book assistants, code context explainers, and text-to-speech.
Product feature icon Thousands of reference materials covering every tech concept you need to stay up to date.
Subscribe now
View plans & pricing

Product Details

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

Packt Subscriptions

See our plans and pricing
Modal Close icon
$19.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
$199.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
$279.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

13 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
Unlock this Book’s Free Benefits in 3 Easy Steps 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

What is included in a Packt subscription? Chevron down icon Chevron up icon

A subscription provides you with full access to view all Packt and licnesed content online, this includes exclusive access to Early Access titles. Depending on the tier chosen you can also earn credits and discounts to use for owning content

How can I cancel my subscription? Chevron down icon Chevron up icon

To cancel your subscription with us simply go to the account page - found in the top right of the page or at https://subscription.packtpub.com/my-account/subscription - From here you will see the ‘cancel subscription’ button in the grey box with your subscription information in.

What are credits? Chevron down icon Chevron up icon

Credits can be earned from reading 40 section of any title within the payment cycle - a month starting from the day of subscription payment. You also earn a Credit every month if you subscribe to our annual or 18 month plans. Credits can be used to buy books DRM free, the same way that you would pay for a book. Your credits can be found in the subscription homepage - subscription.packtpub.com - clicking on ‘the my’ library dropdown and selecting ‘credits’.

What happens if an Early Access Course is cancelled? Chevron down icon Chevron up icon

Projects are rarely cancelled, but sometimes it's unavoidable. If an Early Access course is cancelled or excessively delayed, you can exchange your purchase for another course. For further details, please contact us here.

Where can I send feedback about an Early Access title? Chevron down icon Chevron up icon

If you have any feedback about the product you're reading, or Early Access in general, then please fill out a contact form here and we'll make sure the feedback gets to the right team. 

Can I download the code files for Early Access titles? Chevron down icon Chevron up icon

We try to ensure that all books in Early Access have code available to use, download, and fork on GitHub. This helps us be more agile in the development of the book, and helps keep the often changing code base of new versions and new technologies as up to date as possible. Unfortunately, however, there will be rare cases when it is not possible for us to have downloadable code samples available until publication.

When we publish the book, the code files will also be available to download from the Packt website.

How accurate is the publication date? Chevron down icon Chevron up icon

The publication date is as accurate as we can be at any point in the project. Unfortunately, delays can happen. Often those delays are out of our control, such as changes to the technology code base or delays in the tech release. We do our best to give you an accurate estimate of the publication date at any given time, and as more chapters are delivered, the more accurate the delivery date will become.

How will I know when new chapters are ready? Chevron down icon Chevron up icon

We'll let you know every time there has been an update to a course that you've bought in Early Access. You'll get an email to let you know there has been a new chapter, or a change to a previous chapter. The new chapters are automatically added to your account, so you can also check back there any time you're ready and download or read them online.

I am a Packt subscriber, do I get Early Access? Chevron down icon Chevron up icon

Yes, all Early Access content is fully available through your subscription. You will need to have a paid for or active trial subscription in order to access all titles.

How is Early Access delivered? Chevron down icon Chevron up icon

Early Access is currently only available as a PDF or through our online reader. As we make changes or add new chapters, the files in your Packt account will be updated so you can download them again or view them online immediately.

How do I buy Early Access content? Chevron down icon Chevron up icon

Early Access is a way of us getting our content to you quicker, but the method of buying the Early Access course is still the same. Just find the course you want to buy, go through the check-out steps, and you’ll get a confirmation email from us with information and a link to the relevant Early Access courses.

What is Early Access? Chevron down icon Chevron up icon

Keeping up to date with the latest technology is difficult; new versions, new frameworks, new techniques. This feature gives you a head-start to our content, as it's being created. With Early Access you'll receive each chapter as it's written, and get regular updates throughout the product's development, as well as the final course as soon as it's ready.We created Early Access as a means of giving you the information you need, as soon as it's available. As we go through the process of developing a course, 99% of it can be ready but we can't publish until that last 1% falls in to place. Early Access helps to unlock the potential of our content early, to help you start your learning when you need it most. You not only get access to every chapter as it's delivered, edited, and updated, but you'll also get the finalized, DRM-free product to download in any format you want when it's published. As a member of Packt, you'll also be eligible for our exclusive offers, including a free course every day, and discounts on new and popular titles.

Modal Close icon
Modal Close icon